1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127
|
# frozen_string_literal: true
# The scope class, which handles storing and retrieving variables and types and
# such.
require 'forwardable'
require_relative '../../puppet/parser'
require_relative '../../puppet/parser/templatewrapper'
require_relative '../../puppet/parser/resource'
# This class is part of the internal parser/evaluator/compiler functionality of Puppet.
# It is passed between the various classes that participate in evaluation.
# None of its methods are API except those that are clearly marked as such.
#
# @api public
class Puppet::Parser::Scope
extend Forwardable
# Variables that always exist with nil value even if not set
BUILT_IN_VARS = ['module_name', 'caller_module_name'].freeze
EMPTY_HASH = {}.freeze
Puppet::Util.logmethods(self)
include Puppet::Util::Errors
attr_accessor :source, :resource
attr_reader :compiler
attr_accessor :parent
# Hash of hashes of default values per type name
attr_reader :defaults
# Alias for `compiler.environment`
def environment
@compiler.environment
end
# Alias for `compiler.catalog`
def catalog
@compiler.catalog
end
# Abstract base class for LocalScope and MatchScope
#
class Ephemeral
attr_reader :parent
def initialize(parent = nil)
@parent = parent
end
def is_local_scope?
false
end
def [](name)
if @parent
@parent[name]
end
end
def include?(name)
(@parent and @parent.include?(name))
end
def bound?(name)
false
end
def add_entries_to(target = {}, include_undef = false)
@parent.add_entries_to(target, include_undef) unless @parent.nil?
# do not include match data ($0-$n)
target
end
end
class LocalScope < Ephemeral
def initialize(parent=nil)
super parent
@symbols = {}
end
def [](name)
val = @symbols[name]
val.nil? && !@symbols.include?(name) ? super : val
end
def is_local_scope?
true
end
def []=(name, value)
@symbols[name] = value
end
def include?(name)
bound?(name) || super
end
def delete(name)
@symbols.delete(name)
end
def bound?(name)
@symbols.include?(name)
end
def add_entries_to(target = {}, include_undef = false)
super
@symbols.each do |k, v|
if (v == :undef || v.nil?) && !include_undef
target.delete(k)
else
target[ k ] = v
end
end
target
end
end
class MatchScope < Ephemeral
attr_accessor :match_data
def initialize(parent = nil, match_data = nil)
super parent
@match_data = match_data
end
def is_local_scope?
false
end
def [](name)
if bound?(name)
@match_data[name.to_i]
else
super
end
end
def include?(name)
bound?(name) or super
end
def bound?(name)
# A "match variables" scope reports all numeric variables to be bound if the scope has
# match_data. Without match data the scope is transparent.
#
@match_data && name =~ /^\d+$/
end
def []=(name, value)
# TODO: Bad choice of exception
raise Puppet::ParseError, _("Numerical variables cannot be changed. Attempt to set $%{name}") % { name: name }
end
def delete(name)
# TODO: Bad choice of exception
raise Puppet::ParseError, _("Numerical variables cannot be deleted: Attempt to delete: $%{name}") % { name: name }
end
def add_entries_to(target = {}, include_undef = false)
# do not include match data ($0-$n)
super
end
end
# @api private
class ParameterScope < Ephemeral
class Access
attr_accessor :value
def assigned?
instance_variable_defined?(:@value)
end
end
# A parameter default must be evaluated using a special scope. The scope that is given to this method must
# have a `ParameterScope` as its last ephemeral scope. This method will then push a `MatchScope` while the
# given `expression` is evaluated. The method will catch any throw of `:unevaluated_parameter` and produce
# an error saying that the evaluated parameter X tries to access the unevaluated parameter Y.
#
# @param name [String] the name of the currently evaluated parameter
# @param expression [Puppet::Parser::AST] the expression to evaluate
# @param scope [Puppet::Parser::Scope] a scope where a `ParameterScope` has been pushed
# @return [Object] the result of the evaluation
#
# @api private
def evaluate3x(name, expression, scope)
scope.with_guarded_scope do
bad = catch(:unevaluated_parameter) do
scope.new_match_scope(nil)
return as_read_only { expression.safeevaluate(scope) }
end
parameter_reference_failure(name, bad)
end
end
def evaluate(name, expression, scope, evaluator)
scope.with_guarded_scope do
bad = catch(:unevaluated_parameter) do
scope.new_match_scope(nil)
return as_read_only { evaluator.evaluate(expression, scope) }
end
parameter_reference_failure(name, bad)
end
end
def parameter_reference_failure(from, to)
# Parameters are evaluated in the order they have in the @params hash.
keys = @params.keys
raise Puppet::Error, _("%{callee}: expects a value for parameter $%{to}") % { callee: @callee_name, to: to } if keys.index(to) < keys.index(from)
raise Puppet::Error, _("%{callee}: default expression for $%{from} tries to illegally access not yet evaluated $%{to}") % { callee: @callee_name, from: from, to: to }
end
private :parameter_reference_failure
def initialize(parent, callee_name, param_names)
super(parent)
@callee_name = callee_name
@params = {}
param_names.each { |name| @params[name] = Access.new }
end
def [](name)
access = @params[name]
return super if access.nil?
throw(:unevaluated_parameter, name) unless access.assigned?
access.value
end
def []=(name, value)
raise Puppet::Error, _("Attempt to assign variable %{name} when evaluating parameters") % { name: name } if @read_only
@params[name] ||= Access.new
@params[name].value = value
end
def bound?(name)
@params.include?(name)
end
def include?(name)
@params.include?(name) || super
end
def is_local_scope?
true
end
def as_read_only
read_only = @read_only
@read_only = true
begin
yield
ensure
@read_only = read_only
end
end
def to_hash
Hash[@params.select {|_, access| access.assigned? }.map { |name, access| [name, access.value] }]
end
end
# Returns true if the variable of the given name has a non nil value.
# TODO: This has vague semantics - does the variable exist or not?
# use ['name'] to get nil or value, and if nil check with exist?('name')
# this include? is only useful because of checking against the boolean value false.
#
def include?(name)
catch(:undefined_variable) {
return ! self[name].nil?
}
false
end
# Returns true if the variable of the given name is set to any value (including nil)
#
# @return [Boolean] if variable exists or not
#
def exist?(name)
# Note !! ensure the answer is boolean
!! if name =~ /^(.*)::(.+)$/
class_name = $1
variable_name = $2
return true if class_name == '' && BUILT_IN_VARS.include?(variable_name)
# lookup class, but do not care if it is not evaluated since that will result
# in it not existing anyway. (Tests may run with just scopes and no evaluated classes which
# will result in class_scope for "" not returning topscope).
klass = find_hostclass(class_name)
other_scope = klass.nil? ? nil : class_scope(klass)
if other_scope.nil?
class_name == '' ? compiler.topscope.exist?(variable_name) : false
else
other_scope.exist?(variable_name)
end
else
next_scope = inherited_scope || enclosing_scope
effective_symtable(true).include?(name) || next_scope && next_scope.exist?(name) || BUILT_IN_VARS.include?(name)
end
end
# Returns true if the given name is bound in the current (most nested) scope for assignments.
#
def bound?(name)
# Do not look in ephemeral (match scope), the semantics is to answer if an assignable variable is bound
effective_symtable(false).bound?(name)
end
# Is the value true? This allows us to control the definition of truth
# in one place.
def self.true?(value)
case value
when ''
false
when :undef
false
else
!!value
end
end
# Coerce value to a number, or return `nil` if it isn't one.
def self.number?(value)
case value
when Numeric
value
when /^-?\d+(:?\.\d+|(:?\.\d+)?e\d+)$/
value.to_f
when /^0x[0-9a-f]+$/i
value.to_i(16)
when /^0[0-7]+$/
value.to_i(8)
when /^-?\d+$/
value.to_i
else
nil
end
end
def find_hostclass(name)
environment.known_resource_types.find_hostclass(name)
end
def find_definition(name)
environment.known_resource_types.find_definition(name)
end
def find_global_scope()
# walk upwards until first found node_scope or top_scope
if is_nodescope? || is_topscope?
self
else
next_scope = inherited_scope || enclosing_scope
if next_scope.nil?
# this happens when testing, and there is only a single test scope and no link to any
# other scopes
self
else
next_scope.find_global_scope()
end
end
end
def findresource(type, title = nil)
@compiler.catalog.resource(type, title)
end
# Initialize our new scope. Defaults to having no parent.
def initialize(compiler, source: nil, resource: nil)
if compiler.is_a? Puppet::Parser::AbstractCompiler
@compiler = compiler
else
raise Puppet::DevError, _("you must pass a compiler instance to a new scope object")
end
@source = source
@resource = resource
extend_with_functions_module
# The symbol table for this scope. This is where we store variables.
# @symtable = Ephemeral.new(nil, true)
@symtable = LocalScope.new(nil)
@ephemeral = [ MatchScope.new(@symtable, nil) ]
# All of the defaults set for types. It's a hash of hashes,
# with the first key being the type, then the second key being
# the parameter.
@defaults = Hash.new { |dhash,type|
dhash[type] = {}
}
# The table for storing class singletons. This will only actually
# be used by top scopes and node scopes.
@class_scopes = {}
end
# Store the fact that we've evaluated a class, and store a reference to
# the scope in which it was evaluated, so that we can look it up later.
def class_set(name, scope)
if parent
parent.class_set(name, scope)
else
@class_scopes[name] = scope
end
end
# Return the scope associated with a class. This is just here so
# that subclasses can set their parent scopes to be the scope of
# their parent class, and it's also used when looking up qualified
# variables.
def class_scope(klass)
# They might pass in either the class or class name
k = klass.respond_to?(:name) ? klass.name : klass
@class_scopes[k] || (parent && parent.class_scope(k))
end
# Collect all of the defaults set at any higher scopes.
# This is a different type of lookup because it's
# additive -- it collects all of the defaults, with defaults
# in closer scopes overriding those in later scopes.
#
# The lookupdefaults searches in the the order:
#
# * inherited
# * contained (recursive)
# * self
#
def lookupdefaults(type)
values = {}
# first collect the values from the parents
if parent
parent.lookupdefaults(type).each { |var,value|
values[var] = value
}
end
# then override them with any current values
# this should probably be done differently
if @defaults.include?(type)
@defaults[type].each { |var,value|
values[var] = value
}
end
values
end
# Check if the given value is a known default for the given type
#
def is_default?(type, key, value)
defaults_for_type = @defaults[type]
unless defaults_for_type.nil?
default_param = defaults_for_type[key]
return true if !default_param.nil? && value == default_param.value
end
!parent.nil? && parent.is_default?(type, key, value)
end
# Look up a defined type.
def lookuptype(name)
# This happens a lot, avoid making a call to make a call
krt = environment.known_resource_types
krt.find_definition(name) || krt.find_hostclass(name)
end
def undef_as(x,v)
if v.nil? or v == :undef
x
else
v
end
end
# Lookup a variable within this scope using the Puppet language's
# scoping rules. Variables can be qualified using just as in a
# manifest.
#
# @param [String] name the variable name to lookup
# @param [Hash] hash of options, only internal code should give this
# @param [Boolean] if resolution is of the leaf of a qualified name - only internal code should give this
# @return Object the value of the variable, or if not found; nil if `strict_variables` is false, and thrown :undefined_variable otherwise
#
# @api public
def lookupvar(name, options = EMPTY_HASH)
unless name.is_a? String
raise Puppet::ParseError, _("Scope variable name %{name} is a %{klass}, not a string") % { name: name.inspect, klass: name.class }
end
# If name has '::' in it, it is resolved as a qualified variable
unless (idx = name.index('::')).nil?
# Always drop leading '::' if present as that is how the values are keyed.
return lookup_qualified_variable(idx == 0 ? name[2..-1] : name, options)
end
# At this point, search is for a non qualified (simple) name
table = @ephemeral.last
val = table[name]
return val unless val.nil? && !table.include?(name)
next_scope = inherited_scope || enclosing_scope
if next_scope
next_scope.lookupvar(name, options)
else
variable_not_found(name)
end
end
UNDEFINED_VARIABLES_KIND = 'undefined_variables'
# The exception raised when a throw is uncaught is different in different versions
# of ruby. In >=2.2.0 it is UncaughtThrowError (which did not exist prior to this)
#
UNCAUGHT_THROW_EXCEPTION = defined?(UncaughtThrowError) ? UncaughtThrowError : ArgumentError
def variable_not_found(name, reason=nil)
# Built in variables and numeric variables always exist
if BUILT_IN_VARS.include?(name) || name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME
return nil
end
begin
throw(:undefined_variable, reason)
rescue UNCAUGHT_THROW_EXCEPTION
case Puppet[:strict]
when :off
# do nothing
when :warning
Puppet.warn_once(UNDEFINED_VARIABLES_KIND, _("Variable: %{name}") % { name: name },
_("Undefined variable '%{name}'; %{reason}") % { name: name, reason: reason } )
when :error
raise ArgumentError, _("Undefined variable '%{name}'; %{reason}") % { name: name, reason: reason }
end
end
nil
end
# Retrieves the variable value assigned to the name given as an argument. The name must be a String,
# and namespace can be qualified with '::'. The value is looked up in this scope, its parent scopes,
# or in a specific visible named scope.
#
# @param varname [String] the name of the variable (may be a qualified name using `(ns'::')*varname`
# @param options [Hash] Additional options, not part of api.
# @return [Object] the value assigned to the given varname
# @see #[]=
# @api public
#
def [](varname, options = EMPTY_HASH)
lookupvar(varname, options)
end
# The class scope of the inherited thing of this scope's resource.
#
# @return [Puppet::Parser::Scope] The scope or nil if there is not an inherited scope
def inherited_scope
if resource && resource.type == TYPENAME_CLASS && !resource.resource_type.parent.nil?
qualified_scope(resource.resource_type.parent)
else
nil
end
end
# The enclosing scope (topscope or nodescope) of this scope.
# The enclosing scopes are produced when a class or define is included at
# some point. The parent scope of the included class or define becomes the
# scope in which it was included. The chain of parent scopes is followed
# until a node scope or the topscope is found
#
# @return [Puppet::Parser::Scope] The scope or nil if there is no enclosing scope
def enclosing_scope
if has_enclosing_scope?
if parent.is_topscope? || parent.is_nodescope?
parent
else
parent.enclosing_scope
end
end
end
def is_classscope?
resource && resource.type == TYPENAME_CLASS
end
def is_nodescope?
resource && resource.type == TYPENAME_NODE
end
def is_topscope?
equal?(@compiler.topscope)
end
# @api private
def lookup_qualified_variable(fqn, options)
table = @compiler.qualified_variables
val = table[fqn]
return val if !val.nil? || table.include?(fqn)
# not found - search inherited scope for class
leaf_index = fqn.rindex('::')
unless leaf_index.nil?
leaf_name = fqn[ (leaf_index+2)..-1 ]
class_name = fqn[ 0, leaf_index ]
begin
qs = qualified_scope(class_name)
unless qs.nil?
return qs.get_local_variable(leaf_name) if qs.has_local_variable?(leaf_name)
iscope = qs.inherited_scope
return lookup_qualified_variable("#{iscope.source.name}::#{leaf_name}", options) unless iscope.nil?
end
rescue RuntimeError => e
# because a failure to find the class, or inherited should be reported against given name
return handle_not_found(class_name, leaf_name, options, e.message)
end
end
# report with leading '::' by using empty class_name
return handle_not_found('', fqn, options)
end
# @api private
def has_local_variable?(name)
@ephemeral.last.include?(name)
end
# @api private
def get_local_variable(name)
@ephemeral.last[name]
end
def handle_not_found(class_name, variable_name, position, reason = nil)
unless Puppet[:strict_variables]
# Do not issue warning if strict variables are on, as an error will be raised by variable_not_found
location = if position[:lineproc]
Puppet::Util::Errors.error_location_with_space(nil, position[:lineproc].call)
else
Puppet::Util::Errors.error_location_with_space(position[:file], position[:line])
end
variable_not_found("#{class_name}::#{variable_name}", "#{reason}#{location}")
return nil
end
variable_not_found("#{class_name}::#{variable_name}", reason)
end
def has_enclosing_scope?
! parent.nil?
end
private :has_enclosing_scope?
def qualified_scope(classname)
klass = find_hostclass(classname)
raise _("class %{classname} could not be found") % { classname: classname } unless klass
kscope = class_scope(klass)
raise _("class %{classname} has not been evaluated") % { classname: classname } unless kscope
kscope
end
private :qualified_scope
# Returns a Hash containing all variables and their values, optionally (and
# by default) including the values defined in parent. Local values
# shadow parent values. Ephemeral scopes for match results ($0 - $n) are not included.
# Optionally include the variables that are explicitly set to `undef`.
#
def to_hash(recursive = true, include_undef = false)
if recursive and has_enclosing_scope?
target = enclosing_scope.to_hash(recursive)
if !(inherited = inherited_scope).nil?
target.merge!(inherited.to_hash(recursive))
end
else
target = Hash.new
end
# add all local scopes
@ephemeral.last.add_entries_to(target, include_undef)
target
end
# Create a new scope and set these options.
def newscope(options = {})
compiler.newscope(self, options)
end
def parent_module_name
return nil unless @parent && @parent.source
@parent.source.module_name
end
# Set defaults for a type. The typename should already be downcased,
# so that the syntax is isolated. We don't do any kind of type-checking
# here; instead we let the resource do it when the defaults are used.
def define_settings(type, params)
table = @defaults[type]
# if we got a single param, it'll be in its own array
params = [params] unless params.is_a?(Array)
params.each { |param|
if table.include?(param.name)
raise Puppet::ParseError.new(_("Default already defined for %{type} { %{param} }; cannot redefine") % { type: type, param: param.name }, param.file, param.line)
end
table[param.name] = param
}
end
# Merge all settings for the given _env_name_ into this scope
# @param env_name [Symbol] the name of the environment
# @param set_in_this_scope [Boolean] if the settings variables should also be set in this instance of scope
def merge_settings(env_name, set_in_this_scope=true)
settings = Puppet.settings
table = effective_symtable(false)
global_table = compiler.qualified_variables
all_local = {}
settings.each_key do |name|
next if :name == name
key = name.to_s
value = transform_setting(settings.value_sym(name, env_name))
if set_in_this_scope
table[key] = value
end
all_local[key] = value
# also write the fqn into global table for direct lookup
global_table["settings::#{key}"] = value
end
# set the 'all_local' - a hash of all settings
global_table["settings::all_local"] = all_local
nil
end
def transform_setting(val)
if val.is_a?(String) || val.is_a?(Numeric) || true == val || false == val || nil == val
val
elsif val.is_a?(Array)
val.map {|entry| transform_setting(entry) }
elsif val.is_a?(Hash)
result = {}
val.each {|k,v| result[transform_setting(k)] = transform_setting(v) }
result
else
# not ideal, but required as there are settings values that are special
:undef == val ? nil : val.to_s
end
end
private :transform_setting
VARNAME_TRUSTED = 'trusted'
VARNAME_FACTS = 'facts'
VARNAME_SERVER_FACTS = 'server_facts'
RESERVED_VARIABLE_NAMES = [VARNAME_TRUSTED, VARNAME_FACTS].freeze
TYPENAME_CLASS = 'Class'
TYPENAME_NODE = 'Node'
# Set a variable in the current scope. This will override settings
# in scopes above, but will not allow variables in the current scope
# to be reassigned.
# It's preferred that you use self[]= instead of this; only use this
# when you need to set options.
def setvar(name, value, options = EMPTY_HASH)
if name =~ /^[0-9]+$/
raise Puppet::ParseError.new(_("Cannot assign to a numeric match result variable '$%{name}'") % { name: name }) # unless options[:ephemeral]
end
unless name.is_a? String
raise Puppet::ParseError, _("Scope variable name %{name} is a %{class_type}, not a string") % { name: name.inspect, class_type: name.class }
end
# Check for reserved variable names
if (name == VARNAME_TRUSTED || name == VARNAME_FACTS) && !options[:privileged]
raise Puppet::ParseError, _("Attempt to assign to a reserved variable name: '%{name}'") % { name: name }
end
# Check for server_facts reserved variable name
if name == VARNAME_SERVER_FACTS && !options[:privileged]
raise Puppet::ParseError, _("Attempt to assign to a reserved variable name: '%{name}'") % { name: name }
end
table = effective_symtable(options[:ephemeral])
if table.bound?(name)
error = Puppet::ParseError.new(_("Cannot reassign variable '$%{name}'") % { name: name })
error.file = options[:file] if options[:file]
error.line = options[:line] if options[:line]
raise error
end
table[name] = value
# Assign the qualified name in the environment
# Note that Settings scope has a source set to Boolean true.
#
# Only meaningful to set a fqn globally if table to assign to is the top of the scope's ephemeral stack
if @symtable.equal?(table)
if is_topscope?
# the scope name is '::'
compiler.qualified_variables[name] = value
elsif source.is_a?(Puppet::Resource::Type) && source.type == :hostclass
# the name is the name of the class
sourcename = source.name
compiler.qualified_variables["#{sourcename}::#{name}"] = value
end
end
value
end
def set_trusted(hash)
setvar('trusted', deep_freeze(hash), :privileged => true)
end
def set_facts(hash)
setvar('facts', deep_freeze(hash), :privileged => true)
end
def set_server_facts(hash)
setvar('server_facts', deep_freeze(hash), :privileged => true)
end
# Deeply freezes the given object. The object and its content must be of the types:
# Array, Hash, Numeric, Boolean, Regexp, NilClass, or String. All other types raises an Error.
# (i.e. if they are assignable to Puppet::Pops::Types::Data type).
#
def deep_freeze(object)
case object
when Array
object.each {|v| deep_freeze(v) }
object.freeze
when Hash
object.each {|k, v| deep_freeze(k); deep_freeze(v) }
object.freeze
when NilClass, Numeric, TrueClass, FalseClass
# do nothing
when String
object.freeze
else
raise Puppet::Error, _("Unsupported data type: '%{klass}'") % { klass: object.class }
end
object
end
private :deep_freeze
# Return the effective "table" for setting variables.
# This method returns the first ephemeral "table" that acts as a local scope, or this
# scope's symtable. If the parameter `use_ephemeral` is true, the "top most" ephemeral "table"
# will be returned (irrespective of it being a match scope or a local scope).
#
# @param use_ephemeral [Boolean] whether the top most ephemeral (of any kind) should be used or not
def effective_symtable(use_ephemeral)
s = @ephemeral[-1]
return s || @symtable if use_ephemeral
while s && !s.is_local_scope?()
s = s.parent
end
s || @symtable
end
# Sets the variable value of the name given as an argument to the given value. The value is
# set in the current scope and may shadow a variable with the same name in a visible outer scope.
# It is illegal to re-assign a variable in the same scope. It is illegal to set a variable in some other
# scope/namespace than the scope passed to a method.
#
# @param varname [String] The variable name to which the value is assigned. Must not contain `::`
# @param value [String] The value to assign to the given variable name.
# @param options [Hash] Additional options, not part of api and no longer used.
#
# @api public
#
def []=(varname, value, _ = nil)
setvar(varname, value)
end
# Used mainly for logging
def to_s
# As this is used for logging, this should really not be done in this class at all...
return "Scope(#{@resource})" unless @resource.nil?
# For logging of function-scope - it is now showing the file and line.
detail = Puppet::Pops::PuppetStack.top_of_stack
return "Scope()" if detail.empty?
# shorten the path if possible
path = detail[0]
env_path = nil
env_path = environment.configuration.path_to_env unless (environment.nil? || environment.configuration.nil?)
# check module paths first since they may be in the environment (i.e. they are longer)
module_path = environment.full_modulepath.detect {|m_path| path.start_with?(m_path) }
if module_path
path = "<module>" + path[module_path.length..-1]
elsif env_path && path && path.start_with?(env_path)
path = "<env>" + path[env_path.length..-1]
end
# Make the output appear as "Scope(path, line)"
"Scope(#{[path, detail[1]].join(', ')})"
end
alias_method :inspect, :to_s
# Pop ephemeral scopes up to level and return them
#
# @param level [Integer] a positive integer
# @return [Array] the removed ephemeral scopes
# @api private
def pop_ephemerals(level)
@ephemeral.pop(@ephemeral.size - level)
end
# Push ephemeral scopes onto the ephemeral scope stack
# @param ephemeral_scopes [Array]
# @api private
def push_ephemerals(ephemeral_scopes)
ephemeral_scopes.each { |ephemeral_scope| @ephemeral.push(ephemeral_scope) } unless ephemeral_scopes.nil?
end
def ephemeral_level
@ephemeral.size
end
# TODO: Who calls this?
def new_ephemeral(local_scope = false)
if local_scope
@ephemeral.push(LocalScope.new(@ephemeral.last))
else
@ephemeral.push(MatchScope.new(@ephemeral.last, nil))
end
end
# Execute given block in global scope with no ephemerals present
#
# @yieldparam [Scope] global_scope the global and ephemeral less scope
# @return [Object] the return of the block
#
# @api private
def with_global_scope(&block)
find_global_scope.without_ephemeral_scopes(&block)
end
# Execute given block with a ephemeral scope containing the given variables
# @api private
def with_local_scope(scope_variables)
local = LocalScope.new(@ephemeral.last)
scope_variables.each_pair { |k, v| local[k] = v }
@ephemeral.push(local)
begin
yield(self)
ensure
@ephemeral.pop
end
end
# Execute given block with all ephemeral popped from the ephemeral stack
#
# @api private
def without_ephemeral_scopes
save_ephemeral = @ephemeral
begin
@ephemeral = [ @symtable ]
yield(self)
ensure
@ephemeral = save_ephemeral
end
end
# Nests a parameter scope
# @param [String] callee_name the name of the function, template, or resource that defines the parameters
# @param [Array<String>] param_names list of parameter names
# @yieldparam [ParameterScope] param_scope the nested scope
# @api private
def with_parameter_scope(callee_name, param_names)
param_scope = ParameterScope.new(@ephemeral.last, callee_name, param_names)
with_guarded_scope do
@ephemeral.push(param_scope)
yield(param_scope)
end
end
# Execute given block and ensure that ephemeral level is restored
#
# @return [Object] the return of the block
#
# @api private
def with_guarded_scope
elevel = ephemeral_level
begin
yield
ensure
pop_ephemerals(elevel)
end
end
# Sets match data in the most nested scope (which always is a MatchScope), it clobbers match data already set there
#
def set_match_data(match_data)
@ephemeral.last.match_data = match_data
end
# Nests a match data scope
def new_match_scope(match_data)
@ephemeral.push(MatchScope.new(@ephemeral.last, match_data))
end
def ephemeral_from(match, file = nil, line = nil)
case match
when Hash
# Create local scope ephemeral and set all values from hash
new_ephemeral(true)
match.each {|k,v| setvar(k, v, :file => file, :line => line, :ephemeral => true) }
# Must always have an inner match data scope (that starts out as transparent)
# In 3x slightly wasteful, since a new nested scope is created for a match
# (TODO: Fix that problem)
new_ephemeral(false)
else
raise(ArgumentError,_("Invalid regex match data. Got a %{klass}") % { klass: match.class }) unless match.is_a?(MatchData)
# Create a match ephemeral and set values from match data
new_match_scope(match)
end
end
# @api private
def find_resource_type(type)
raise Puppet::DevError, _("Scope#find_resource_type() is no longer supported, use Puppet::Pops::Evaluator::Runtime3ResourceSupport instead")
end
# @api private
def find_builtin_resource_type(type)
raise Puppet::DevError, _("Scope#find_builtin_resource_type() is no longer supported, use Puppet::Pops::Evaluator::Runtime3ResourceSupport instead")
end
# @api private
def find_defined_resource_type(type)
raise Puppet::DevError, _("Scope#find_defined_resource_type() is no longer supported, use Puppet::Pops::Evaluator::Runtime3ResourceSupport instead")
end
def method_missing(method, *args, &block)
method.to_s =~ /^function_(.*)$/
name = $1
super unless name
super unless Puppet::Parser::Functions.function(name)
# In odd circumstances, this might not end up defined by the previous
# method, so we might as well be certain.
if respond_to? method
send(method, *args)
else
raise Puppet::DevError, _("Function %{name} not defined despite being loaded!") % { name: name }
end
end
# To be removed when enough time has passed after puppet 5.0.0
# @api private
def resolve_type_and_titles(type, titles)
raise Puppet::DevError, _("Scope#resolve_type_and_title() is no longer supported, use Puppet::Pops::Evaluator::Runtime3ResourceSupport instead")
end
# Transforms references to classes to the form suitable for
# lookup in the compiler.
#
# Makes names passed in the names array absolute if they are relative.
#
# Transforms Class[] and Resource[] type references to class name
# or raises an error if a Class[] is unspecific, if a Resource is not
# a 'class' resource, or if unspecific (no title).
#
#
# @param names [Array<String>] names to (optionally) make absolute
# @return [Array<String>] names after transformation
#
def transform_and_assert_classnames(names)
names.map do |name|
case name
when NilClass
raise ArgumentError, _("Cannot use undef as a class name")
when String
raise ArgumentError, _("Cannot use empty string as a class name") if name.empty?
name.sub(/^([^:]{1,2})/, '::\1')
when Puppet::Resource
assert_class_and_title(name.type, name.title)
name.title.sub(/^([^:]{1,2})/, '::\1')
when Puppet::Pops::Types::PClassType
#TRANSLATORS "Class" and "Type" are Puppet keywords and should not be translated
raise ArgumentError, _("Cannot use an unspecific Class[] Type") unless name.class_name
name.class_name.sub(/^([^:]{1,2})/, '::\1')
when Puppet::Pops::Types::PResourceType
assert_class_and_title(name.type_name, name.title)
name.title.sub(/^([^:]{1,2})/, '::\1')
end.downcase
end
end
# Calls a 3.x or 4.x function by name with arguments given in an array using the 4.x calling convention
# and returns the result.
# Note that it is the caller's responsibility to rescue the given ArgumentError and provide location information
# to aid the user find the problem. The problem is otherwise reported against the source location that
# invoked the function that ultimately called this method.
#
# @return [Object] the result of the called function
# @raise ArgumentError if the function does not exist
def call_function(func_name, args, &block)
Puppet::Pops::Parser::EvaluatingParser.new.evaluator.external_call_function(func_name, args, self, &block)
end
private
def assert_class_and_title(type_name, title)
if type_name.nil? || type_name == ''
#TRANSLATORS "Resource" is a class name and should not be translated
raise ArgumentError, _("Cannot use an unspecific Resource[] where a Resource['class', name] is expected")
end
unless type_name =~ /^[Cc]lass$/
#TRANSLATORS "Resource" is a class name and should not be translated
raise ArgumentError, _("Cannot use a Resource[%{type_name}] where a Resource['class', name] is expected") % { type_name: type_name }
end
if title.nil?
#TRANSLATORS "Resource" is a class name and should not be translated
raise ArgumentError, _("Cannot use an unspecific Resource['class'] where a Resource['class', name] is expected")
end
end
def extend_with_functions_module
root = Puppet.lookup(:root_environment)
extend Puppet::Parser::Functions.environment_module(root)
extend Puppet::Parser::Functions.environment_module(environment) if environment != root
end
end
|