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
|
require 'puppet/util/docs'
require 'puppet/indirector/envelope'
require 'puppet/indirector/request'
require 'puppet/util/cacher'
# The class that connects functional classes with their different collection
# back-ends. Each indirection has a set of associated terminus classes,
# each of which is a subclass of Puppet::Indirector::Terminus.
class Puppet::Indirector::Indirection
include Puppet::Util::Cacher
include Puppet::Util::Docs
@@indirections = []
# Find an indirection by name. This is provided so that Terminus classes
# can specifically hook up with the indirections they are associated with.
def self.instance(name)
@@indirections.find { |i| i.name == name }
end
# Return a list of all known indirections. Used to generate the
# reference.
def self.instances
@@indirections.collect { |i| i.name }
end
# Find an indirected model by name. This is provided so that Terminus classes
# can specifically hook up with the indirections they are associated with.
def self.model(name)
return nil unless match = @@indirections.find { |i| i.name == name }
match.model
end
attr_accessor :name, :model
# Create and return our cache terminus.
def cache
raise(Puppet::DevError, "Tried to cache when no cache class was set") unless cache_class
terminus(cache_class)
end
# Should we use a cache?
def cache?
cache_class ? true : false
end
attr_reader :cache_class
# Define a terminus class to be used for caching.
def cache_class=(class_name)
validate_terminus_class(class_name) if class_name
@cache_class = class_name
end
# This is only used for testing.
def delete
@@indirections.delete(self) if @@indirections.include?(self)
end
# Set the time-to-live for instances created through this indirection.
def ttl=(value)
raise ArgumentError, "Indirection TTL must be an integer" unless value.is_a?(Fixnum)
@ttl = value
end
# Default to the runinterval for the ttl.
def ttl
unless defined?(@ttl)
@ttl = Puppet[:runinterval].to_i
end
@ttl
end
# Calculate the expiration date for a returned instance.
def expiration
Time.now + ttl
end
# Generate the full doc string.
def doc
text = ""
if defined? @doc and @doc
text += scrub(@doc) + "\n\n"
end
if s = terminus_setting()
text += "* **Terminus Setting**: %s" % terminus_setting
end
text
end
def initialize(model, name, options = {})
@model = model
@name = name
@cache_class = nil
@terminus_class = nil
raise(ArgumentError, "Indirection %s is already defined" % @name) if @@indirections.find { |i| i.name == @name }
@@indirections << self
if mod = options[:extend]
extend(mod)
options.delete(:extend)
end
# This is currently only used for cache_class and terminus_class.
options.each do |name, value|
begin
send(name.to_s + "=", value)
rescue NoMethodError
raise ArgumentError, "%s is not a valid Indirection parameter" % name
end
end
end
# Set up our request object.
def request(*args)
Puppet::Indirector::Request.new(self.name, *args)
end
# Return the singleton terminus for this indirection.
def terminus(terminus_name = nil)
# Get the name of the terminus.
unless terminus_name ||= terminus_class
raise Puppet::DevError, "No terminus specified for %s; cannot redirect" % self.name
end
return termini[terminus_name] ||= make_terminus(terminus_name)
end
# This can be used to select the terminus class.
attr_accessor :terminus_setting
# Determine the terminus class.
def terminus_class
unless @terminus_class
if setting = self.terminus_setting
self.terminus_class = Puppet.settings[setting].to_sym
else
raise Puppet::DevError, "No terminus class nor terminus setting was provided for indirection %s" % self.name
end
end
@terminus_class
end
def reset_terminus_class
@terminus_class = nil
end
# Specify the terminus class to use.
def terminus_class=(klass)
validate_terminus_class(klass)
@terminus_class = klass
end
# This is used by terminus_class= and cache=.
def validate_terminus_class(terminus_class)
unless terminus_class and terminus_class.to_s != ""
raise ArgumentError, "Invalid terminus name %s" % terminus_class.inspect
end
unless Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class)
raise ArgumentError, "Could not find terminus %s for indirection %s" % [terminus_class, self.name]
end
end
# Expire a cached object, if one is cached. Note that we don't actually
# remove it, we expire it and write it back out to disk. This way people
# can still use the expired object if they want.
def expire(key, *args)
request = request(:expire, key, *args)
return nil unless cache?
return nil unless instance = cache.find(request(:find, key, *args))
Puppet.info "Expiring the %s cache of %s" % [self.name, instance.name]
# Set an expiration date in the past
instance.expiration = Time.now - 60
cache.save(request(:save, instance, *args))
end
# Search for an instance in the appropriate terminus, caching the
# results if caching is configured..
def find(key, *args)
request = request(:find, key, *args)
terminus = prepare(request)
begin
if result = find_in_cache(request)
return result
end
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Cached %s for %s failed: %s" % [self.name, request.key, detail]
end
# Otherwise, return the result from the terminus, caching if appropriate.
if ! request.ignore_terminus? and result = terminus.find(request)
result.expiration ||= self.expiration
if cache? and request.use_cache?
Puppet.info "Caching %s for %s" % [self.name, request.key]
cache.save request(:save, result, *args)
end
return terminus.respond_to?(:filter) ? terminus.filter(result) : result
end
return nil
end
def find_in_cache(request)
# See if our instance is in the cache and up to date.
return nil unless cache? and ! request.ignore_cache? and cached = cache.find(request)
if cached.expired?
Puppet.info "Not using expired %s for %s from cache; expired at %s" % [self.name, request.key, cached.expiration]
return nil
end
Puppet.debug "Using cached %s for %s" % [self.name, request.key]
return cached
end
# Remove something via the terminus.
def destroy(key, *args)
request = request(:destroy, key, *args)
terminus = prepare(request)
result = terminus.destroy(request)
if cache? and cached = cache.find(request(:find, key, *args))
# Reuse the existing request, since it's equivalent.
cache.destroy(request)
end
result
end
# Search for more than one instance. Should always return an array.
def search(key, *args)
request = request(:search, key, *args)
terminus = prepare(request)
if result = terminus.search(request)
raise Puppet::DevError, "Search results from terminus %s are not an array" % terminus.name unless result.is_a?(Array)
result.each do |instance|
instance.expiration ||= self.expiration
end
return result
end
end
# Save the instance in the appropriate terminus. This method is
# normally an instance method on the indirected class.
def save(key, instance = nil)
request = request(:save, key, instance)
terminus = prepare(request)
result = terminus.save(request)
# If caching is enabled, save our document there
cache.save(request) if cache?
result
end
private
# Check authorization if there's a hook available; fail if there is one
# and it returns false.
def check_authorization(request, terminus)
# At this point, we're assuming authorization makes no sense without
# client information.
return unless request.node
# This is only to authorize via a terminus-specific authorization hook.
return unless terminus.respond_to?(:authorized?)
unless terminus.authorized?(request)
msg = "Not authorized to call %s on %s" % [request.method, request.to_s]
unless request.options.empty?
msg += " with %s" % request.options.inspect
end
raise ArgumentError, msg
end
end
# Setup a request, pick the appropriate terminus, check the request's authorization, and return it.
def prepare(request)
# Pick our terminus.
if respond_to?(:select_terminus)
unless terminus_name = select_terminus(request)
raise ArgumentError, "Could not determine appropriate terminus for %s" % request
end
else
terminus_name = terminus_class
end
dest_terminus = terminus(terminus_name)
check_authorization(request, dest_terminus)
return dest_terminus
end
# Create a new terminus instance.
def make_terminus(terminus_class)
# Load our terminus class.
unless klass = Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class)
raise ArgumentError, "Could not find terminus %s for indirection %s" % [terminus_class, self.name]
end
return klass.new
end
# Cache our terminus instances indefinitely, but make it easy to clean them up.
cached_attr(:termini) { Hash.new }
end
|