summaryrefslogtreecommitdiffstats
path: root/lib/puppet/resource/type.rb
blob: 8b154ce951f14159d69306108d96cc80c49d6caa (plain)
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
require 'puppet/parser/parser'
require 'puppet/util/warnings'
require 'puppet/util/errors'
require 'puppet/util/inline_docs'
require 'puppet/parser/ast/leaf'
require 'puppet/dsl'

class Puppet::Resource::Type
  Puppet::ResourceType = self
  include Puppet::Util::InlineDocs
  include Puppet::Util::Warnings
  include Puppet::Util::Errors

  RESOURCE_SUPERTYPES = [:hostclass, :node, :definition]

  attr_accessor :file, :line, :doc, :code, :ruby_code, :parent, :resource_type_collection
  attr_reader :type, :namespace, :arguments, :behaves_like, :module_name

  RESOURCE_SUPERTYPES.each do |t|
    define_method("#{t}?") { self.type == t }
  end

  require 'puppet/indirector'
  extend Puppet::Indirector
  indirects :resource_type, :terminus_class => :parser

  def self.from_pson(data)
    name = data.delete('name') or raise ArgumentError, "Resource Type names must be specified"
    type = data.delete('type') || "definition"

    data = data.inject({}) { |result, ary| result[ary[0].intern] = ary[1]; result }

    new(type, name, data)
  end

  def to_pson_data_hash
    data = [:doc, :line, :file, :parent].inject({}) do |hash, param|
      next hash unless (value = self.send(param)) and (value != "")
      hash[param.to_s] = value
      hash
    end

    data['arguments'] = arguments.dup unless arguments.empty?

    data['name'] = name
    data['type'] = type

    data
  end

  def to_pson(*args)
    to_pson_data_hash.to_pson(*args)
  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 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(:namespace => namespace, :source => self, :resource => resource, :dynamic => !static_parent) unless resource.title == :main
    scope.compiler.add_class(name) unless definition?

    set_resource_parameters(resource, scope)

    code.safeevaluate(scope) if code

    evaluate_ruby_code(resource, scope) if ruby_code
  end

  def initialize(type, name, options = {})
    @type = type.to_s.downcase.to_sym
    raise ArgumentError, "Invalid resource supertype '#{type}'" unless RESOURCE_SUPERTYPES.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|
      next unless value = options[param]
      send(param.to_s + "=", value)
    end

    set_arguments(options[:arguments])

    @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?

    @name =~ 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" unless type == :hostclass
    fail "#{other.name} is not a class; cannot add code from it" unless other.type == :hostclass
    fail "Cannot have code outside of a class/node/define because 'freeze_main' is enabled" if name == "" and Puppet.settings[:freeze_main]

    if parent and other.parent and parent != other.parent
      fail "Cannot merge classes with different parent classes (#{name} => #{parent} vs. #{other.name} => #{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

    array_class = Puppet::Parser::AST::ASTArray
    self.code = array_class.new(:children => [self.code]) unless self.code.is_a?(array_class)

    if other.code.is_a?(array_class)
      code.children += other.code.children
    else
      code.children << other.code
    end
  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)
    type == :definition and raise ArgumentError, "Cannot create resources for defined resource types"
    resource_type = type == :hostclass ? :class : :node

    # 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 resource = scope.catalog.resource(resource_type, name) and !parameters
      return resource
    end
    resource = Puppet::Parser::Resource.new(resource_type, name, :scope => scope, :source => self)
    assign_parameter_values(parameters, resource)
    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.tag(*resource.tags)
    end
  end

  def name
    return @name unless @name.is_a?(Regexp)
    @name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'')
  end

  def name_is_regex?
    @name.is_a?(Regexp)
  end

  def assign_parameter_values(parameters, resource)
    return unless parameters
    scope = resource.scope || {}

    # It'd be nice to assign default parameter values here,
    # but we can't because they often rely on local variables
    # created during set_resource_parameters.
    parameters.each do |name, value|
      resource.set_parameter name, value
    end
  end

  # MQR TODO:
  #
  # The change(s) introduced by the fix for #4270 are mostly silly & should be 
  # removed, though we didn't realize it at the time.  If it can be established/
  # ensured that nodes never call parent_type and that resource_types are always
  # (as they should be) members of exactly one resource_type_collection the 
  # following method could / should be replaced with:
  #
  # def parent_type
  #   @parent_type ||= parent && (
  #     resource_type_collection.find_or_load([name],parent,type.to_sym) ||
  #     fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{resource_type_collection.environment}"
  #   )
  # end
  #
  # ...and then the rest of the changes around passing in scope reverted.
  #
  def parent_type(scope = nil)
    return nil unless parent

    unless @parent_type
      raise "Must pass scope to parent_type when called first time" unless scope
      unless @parent_type = scope.environment.known_resource_types.send("find_#{type}", [name], parent)
        fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{scope.environment}"
      end
    end

    @parent_type
  end

  # Set any arguments passed by the resource as variables in the scope.
  def set_resource_parameters(resource, scope)
    set = {}
    resource.to_hash.each do |param, value|
      param = param.to_sym
      fail Puppet::ParseError, "#{resource.ref} does not accept attribute #{param}" unless valid_parameter?(param)

      exceptwrap { scope[param.to_s] = value }

      set[param] = true
    end

    if @type == :hostclass
      scope["title"] = resource.title.to_s.downcase unless set.include? :title
      scope["name"] =  resource.name.to_s.downcase  unless set.include? :name
    else
      scope["title"] = resource.title               unless set.include? :title
      scope["name"] =  resource.name                unless set.include? :name
    end
    scope["module_name"] = module_name if module_name and ! set.include? :module_name

    if caller_name = scope.parent_module_name and ! set.include?(:caller_module_name)
      scope["caller_module_name"] = caller_name
    end
    scope.class_set(self.name,scope) if hostclass? or node?

    # Evaluate the default parameters, now that all other variables are set
    default_params = resource.set_default_parameters(scope)
    default_params.each { |param| scope[param.to_s] = resource[param] }

    # This has to come after the above parameters so that default values
    # can use their values
    resource.validate_complete
  end

  # Check whether a given argument is valid.
  def valid_parameter?(param)
    param = param.to_s

    return true if param == "name"
    return true if Puppet::Type.metaparam?(param)
    return false unless defined?(@arguments)
    return(arguments.include?(param) ? true : false)
  end

  def set_arguments(arguments)
    @arguments = {}
    return if arguments.nil?

    arguments.each do |arg, default|
      arg = arg.to_s
      warn_if_metaparam(arg, default)
      @arguments[arg] = default
    end
  end

  private

  def convert_from_ast(name)
    value = name.value
    if value.is_a?(Puppet::Parser::AST::Regex)
      name = value.value
    else
      name = value
    end
  end

  def evaluate_parent_type(resource)
    return unless klass = parent_type(resource.scope) and parent_resource = resource.scope.compiler.catalog.resource(:class, klass.name) || resource.scope.compiler.catalog.resource(:node, klass.name)
    parent_resource.evaluate unless parent_resource.evaluated?
    parent_scope(resource.scope, klass)
  end

  def evaluate_ruby_code(resource, scope)
    Puppet::DSL::ResourceAPI.new(resource, scope, ruby_code).evaluate
  end

  # Split an fq name into a namespace and name
  def namesplit(fullname)
    ary = fullname.split("::")
    n = ary.pop || ""
    ns = ary.join("::")
    return ns, n
  end

  def parent_scope(scope, klass)
    scope.class_scope(klass) || raise(Puppet::DevError, "Could not find scope for #{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, ignored_shortname = @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"
    else
      raise Puppet::ParseError, "#{param} is a metaparameter; please choose another parameter name in the #{self.name} definition"
    end
  end
end