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
|
require_relative '../../puppet/parser'
require_relative '../../puppet/util/warnings'
require_relative '../../puppet/util/errors'
require_relative '../../puppet/parser/ast/leaf'
# Puppet::Resource::Type represents nodes, classes and defined types.
#
# @api public
class Puppet::Resource::Type
Puppet::ResourceType = self
include Puppet::Util::Warnings
include Puppet::Util::Errors
RESOURCE_KINDS = [:hostclass, :node, :definition]
# Map the names used in our documentation to the names used internally
RESOURCE_KINDS_TO_EXTERNAL_NAMES = {
:hostclass => "class",
:node => "node",
:definition => "defined_type"
}
RESOURCE_EXTERNAL_NAMES_TO_KINDS = RESOURCE_KINDS_TO_EXTERNAL_NAMES.invert
NAME = 'name'.freeze
TITLE = 'title'.freeze
MODULE_NAME = 'module_name'.freeze
CALLER_MODULE_NAME = 'caller_module_name'.freeze
PARAMETERS = 'parameters'.freeze
KIND = 'kind'.freeze
NODES = 'nodes'.freeze
DOUBLE_COLON = '::'.freeze
EMPTY_ARRAY = [].freeze
attr_accessor :file, :line, :doc, :code, :parent, :resource_type_collection, :override
attr_reader :namespace, :arguments, :behaves_like, :module_name
# Map from argument (aka parameter) names to Puppet Type
# @return [Hash<Symbol, Puppet::Pops::Types::PAnyType] map from name to type
#
attr_reader :argument_types
# This should probably be renamed to 'kind' eventually, in accordance with the changes
# made for serialization and API usability (#14137). At the moment that seems like
# it would touch a whole lot of places in the code, though. --cprice 2012-04-23
attr_reader :type
RESOURCE_KINDS.each do |t|
define_method("#{t}?") { self.type == t }
end
# Are we a child of the passed class? Do a recursive search up our
# parentage tree to figure it out.
def child_of?(klass)
return true if override
return false unless parent
return(klass == parent_type ? true : parent_type.child_of?(klass))
end
# Now evaluate the code associated with this class or definition.
def evaluate_code(resource)
static_parent = evaluate_parent_type(resource)
scope = static_parent || resource.scope
scope = scope.newscope(:source => self, :resource => resource) unless resource.title == :main
scope.compiler.add_class(name) unless definition?
set_resource_parameters(resource, scope)
resource.add_edge_to_stage
if code
if @match # Only bother setting up the ephemeral scope if there are match variables to add into it
scope.with_guarded_scope do
scope.ephemeral_from(@match, file, line)
code.safeevaluate(scope)
end
else
code.safeevaluate(scope)
end
end
end
def initialize(type, name, options = {})
@type = type.to_s.downcase.to_sym
raise ArgumentError, _("Invalid resource supertype '%{type}'") % { type: type } unless RESOURCE_KINDS.include?(@type)
name = convert_from_ast(name) if name.is_a?(Puppet::Parser::AST::HostName)
set_name_and_namespace(name)
[:code, :doc, :line, :file, :parent].each do |param|
value = options[param]
next unless value
send(param.to_s + '=', value)
end
set_arguments(options[:arguments])
set_argument_types(options[:argument_types])
@match = nil
@module_name = options[:module_name]
end
# This is only used for node names, and really only when the node name
# is a regexp.
def match(string)
return string.to_s.downcase == name unless name_is_regex?
@match = @name.match(string)
end
# Add code from a new instance to our code.
def merge(other)
fail _("%{name} is not a class; cannot add code to it") % { name: name } unless type == :hostclass
fail _("%{name} is not a class; cannot add code from it") % { name: other.name } unless other.type == :hostclass
if name == "" && Puppet.settings[:freeze_main]
# It is ok to merge definitions into main even if freeze is on (definitions are nodes, classes, defines, functions, and types)
unless other.code.is_definitions_only?
fail _("Cannot have code outside of a class/node/define because 'freeze_main' is enabled")
end
end
if parent and other.parent and parent != other.parent
fail _("Cannot merge classes with different parent classes (%{name} => %{parent} vs. %{other_name} => %{other_parent})") % { name: name, parent: parent, other_name: other.name, other_parent: other.parent }
end
# We know they're either equal or only one is set, so keep whichever parent is specified.
self.parent ||= other.parent
if other.doc
self.doc ||= ""
self.doc += other.doc
end
# This might just be an empty, stub class.
return unless other.code
unless self.code
self.code = other.code
return
end
self.code = Puppet::Parser::ParserFactory.code_merger.concatenate([self, other])
end
# Make an instance of the resource type, and place it in the catalog
# if it isn't in the catalog already. This is only possible for
# classes and nodes. No parameters are be supplied--if this is a
# parameterized class, then all parameters take on their default
# values.
def ensure_in_catalog(scope, parameters=nil)
resource_type =
case type
when :definition
raise ArgumentError, _('Cannot create resources for defined resource types')
when :hostclass
:class
when :node
:node
end
# Do nothing if the resource already exists; this makes sure we don't
# get multiple copies of the class resource, which helps provide the
# singleton nature of classes.
# we should not do this for classes with parameters
# if parameters are passed, we should still try to create the resource
# even if it exists so that we can fail
# this prevents us from being able to combine param classes with include
if parameters.nil?
resource = scope.catalog.resource(resource_type, name)
return resource unless resource.nil?
elsif parameters.is_a?(Hash)
parameters = parameters.map {|k, v| Puppet::Parser::Resource::Param.new(:name => k, :value => v, :source => self)}
end
resource = Puppet::Parser::Resource.new(resource_type, name, :scope => scope, :source => self, :parameters => parameters)
instantiate_resource(scope, resource)
scope.compiler.add_resource(scope, resource)
resource
end
def instantiate_resource(scope, resource)
# Make sure our parent class has been evaluated, if we have one.
if parent && !scope.catalog.resource(resource.type, parent)
parent_type(scope).ensure_in_catalog(scope)
end
if ['Class', 'Node'].include? resource.type
scope.catalog.merge_tags_from(resource)
end
end
def name
if type == :node && name_is_regex?
"__node_regexp__#{@name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'')}"
else
@name
end
end
def name_is_regex?
@name.is_a?(Regexp)
end
def parent_type(scope = nil)
return nil unless parent
@parent_type ||= scope.environment.known_resource_types.send("find_#{type}", parent) ||
fail(Puppet::ParseError, _("Could not find parent resource type '%{parent}' of type %{parent_type} in %{env}") % { parent: parent, parent_type: type, env: scope.environment })
end
# Validate and set any arguments passed by the resource as variables in the scope.
#
# This method is known to only be used on the server/compile side.
#
# @param resource [Puppet::Parser::Resource] the resource
# @param scope [Puppet::Parser::Scope] the scope
#
# @api private
def set_resource_parameters(resource, scope)
# Inject parameters from using external lookup
modname = resource[:module_name] || module_name
scope[MODULE_NAME] = modname unless modname.nil?
caller_name = resource[:caller_module_name] || scope.parent_module_name
scope[CALLER_MODULE_NAME] = caller_name unless caller_name.nil?
inject_external_parameters(resource, scope)
if @type == :hostclass
scope[TITLE] = resource.title.to_s.downcase
scope[NAME] = resource.name.to_s.downcase
else
scope[TITLE] = resource.title
scope[NAME] = resource.name
end
scope.class_set(self.name,scope) if hostclass? || node?
param_hash = scope.with_parameter_scope(resource.to_s, arguments.keys) do |param_scope|
# Assign directly to the parameter scope to avoid scope parameter validation at this point. It
# will happen anyway when the values are assigned to the scope after the parameter scoped has
# been popped.
resource.each { |k, v| param_scope[k.to_s] = v.value unless k == :name || k == :title }
assign_defaults(resource, param_scope, scope)
param_scope.to_hash
end
validate_resource_hash(resource, param_hash)
# Assign parameter values to current scope
param_hash.each { |param, value| exceptwrap { scope[param] = value }}
end
# Lookup and inject parameters from external scope
# @param resource [Puppet::Parser::Resource] the resource
# @param scope [Puppet::Parser::Scope] the scope
def inject_external_parameters(resource, scope)
# Only lookup parameters for host classes
return unless type == :hostclass
parameters = resource.parameters
arguments.each do |param_name, default|
sym_name = param_name.to_sym
param = parameters[sym_name]
next unless param.nil? || param.value.nil?
catch(:no_such_key) do
bound_value = Puppet::Pops::Lookup.search_and_merge("#{name}::#{param_name}", Puppet::Pops::Lookup::Invocation.new(scope), nil)
# Assign bound value but don't let an undef trump a default expression
resource[sym_name] = bound_value unless bound_value.nil? && !default.nil?
end
end
end
private :inject_external_parameters
def assign_defaults(resource, param_scope, scope)
return unless resource.is_a?(Puppet::Parser::Resource)
parameters = resource.parameters
arguments.each do |param_name, default|
next if default.nil?
name = param_name.to_sym
param = parameters[name]
next unless param.nil? || param.value.nil?
value = exceptwrap { param_scope.evaluate3x(param_name, default, scope) }
resource[name] = value
param_scope[param_name] = value
end
end
private :assign_defaults
def validate_resource_hash(resource, resource_hash)
Puppet::Pops::Types::TypeMismatchDescriber.validate_parameters(resource.to_s, parameter_struct, resource_hash, false)
end
private :validate_resource_hash
# Validate that all parameters given to the resource are correct
# @param resource [Puppet::Resource] the resource to validate
def validate_resource(resource)
# Since Sensitive values have special encoding (in a separate parameter) an unwrapped sensitive value must be
# recreated as a Sensitive in order to perform correct type checking.
sensitives = Set.new(resource.sensitive_parameters)
validate_resource_hash(resource,
Hash[resource.parameters.map do |name, value|
value_to_validate = sensitives.include?(name) ? Puppet::Pops::Types::PSensitiveType::Sensitive.new(value.value) : value.value
[name.to_s, value_to_validate]
end
])
end
# Check whether a given argument is valid.
def valid_parameter?(param)
parameter_struct.hashed_elements.include?(param.to_s)
end
def set_arguments(arguments)
@arguments = {}
@parameter_struct = nil
return if arguments.nil?
arguments.each do |arg, default|
arg = arg.to_s
warn_if_metaparam(arg, default)
@arguments[arg] = default
end
end
# Sets the argument name to Puppet Type hash used for type checking.
# Names must correspond to available arguments (they must be defined first).
# Arguments not mentioned will not be type-checked.
#
def set_argument_types(name_to_type_hash)
@argument_types = {}
@parameter_struct = nil
return unless name_to_type_hash
name_to_type_hash.each do |name, t|
# catch internal errors
unless @arguments.include?(name)
raise Puppet::DevError, _("Parameter '%{name}' is given a type, but is not a valid parameter.") % { name: name }
end
unless t.is_a? Puppet::Pops::Types::PAnyType
raise Puppet::DevError, _("Parameter '%{name}' is given a type that is not a Puppet Type, got %{class_name}") % { name: name, class_name: t.class }
end
@argument_types[name] = t
end
end
private
def convert_from_ast(name)
value = name.value
if value.is_a?(Puppet::Parser::AST::Regex)
value.value
else
value
end
end
def evaluate_parent_type(resource)
klass = parent_type(resource.scope)
parent_resource = resource.scope.compiler.catalog.resource(:class, klass.name) || resource.scope.compiler.catalog.resource(:node, klass.name) if klass
return unless klass && parent_resource
parent_resource.evaluate unless parent_resource.evaluated?
parent_scope(resource.scope, klass)
end
# Split an fq name into a namespace and name
def namesplit(fullname)
ary = fullname.split(DOUBLE_COLON)
n = ary.pop || ""
ns = ary.join(DOUBLE_COLON)
return ns, n
end
def parent_scope(scope, klass)
scope.class_scope(klass) || raise(Puppet::DevError, _("Could not find scope for %{class_name}") % { class_name: klass.name })
end
def set_name_and_namespace(name)
if name.is_a?(Regexp)
@name = name
@namespace = ""
else
@name = name.to_s.downcase
# Note we're doing something somewhat weird here -- we're setting
# the class's namespace to its fully qualified name. This means
# anything inside that class starts looking in that namespace first.
@namespace, _ = @type == :hostclass ? [@name, ''] : namesplit(@name)
end
end
def warn_if_metaparam(param, default)
return unless Puppet::Type.metaparamclass(param)
if default
warnonce _("%{param} is a metaparam; this value will inherit to all contained resources in the %{name} definition") % { param: param, name: self.name }
else
raise Puppet::ParseError, _("%{param} is a metaparameter; please choose another parameter name in the %{name} definition") % { param: param, name: self.name }
end
end
def parameter_struct
@parameter_struct ||= create_params_struct
end
def create_params_struct
arg_types = argument_types
type_factory = Puppet::Pops::Types::TypeFactory
members = { type_factory.optional(type_factory.string(NAME)) => type_factory.any }
Puppet::Type.eachmetaparam do |name|
# TODO: Once meta parameters are typed, this should change to reflect that type
members[name.to_s] = type_factory.any
end
arguments.each_pair do |name, default|
key_type = type_factory.string(name.to_s)
key_type = type_factory.optional(key_type) unless default.nil?
arg_type = arg_types[name]
arg_type = type_factory.any if arg_type.nil?
members[key_type] = arg_type
end
type_factory.struct(members)
end
private :create_params_struct
end
|