/
filemapper.rb
317 lines (271 loc) · 10.2 KB
/
filemapper.rb
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
require 'puppet/util/filetype'
# Forward declaration
module PuppetX; end
module PuppetX::FileMapper
# Copy all desired resource properties into this resource for generation upon flush
#
# This method is necessary for the provider to be ensurable
def create
raise Puppet::Error, "#{self.class} is in an error state" if self.class.failed?
@resource.class.validproperties.each do |property|
if value = @resource.should(property)
@property_hash[property] = value
end
end
self.dirty!
end
# Use the prefetched status to determine of the resource exists.
#
# This method is necessary for the provider to be ensurable
#
# @return [TrueClass || FalseClass]
def exists?
@property_hash[:ensure] and @property_hash[:ensure] == :present
end
# Update the property hash to mark this resource as absent for flushing
#
# This method is necessary for the provider to be ensurable
def destroy
@property_hash[:ensure] = :absent
self.dirty!
end
# Mark the file associated with this resource as dirty
def dirty!
file = select_file
self.class.dirty_file! file
end
# When processing on this resource is complete, trigger a flush on the file
# that this resource belongs to.
def flush
self.class.flush_file(self.select_file)
end
def self.included(klass)
klass.extend PuppetX::FileMapper::ClassMethods
klass.mk_property_methods
klass.initvars
end
module ClassMethods
# @!attribute [rw] unlink_empty_files
# @return [TrueClass || FalseClass] Whether empty files will be removed
attr_accessor :unlink_empty_files
# @!attribute [rw] filetype
# @return [Symbol] The FileType to use when interacting with target files
attr_accessor :filetype
# @!attribute [r] mapped_files
# @return [Hash<filepath => Hash<:dirty => Bool, :filetype => Filetype>>]
# A data structure representing the file paths and filetypes backing this
# provider.
attr_reader :mapped_files
def initvars
@mapped_files = Hash.new {|h, k| h[k] = {}}
@unlink_empty_files = false
@filetype = :flat
@failed = false
@all_providers = []
end
def failed?
@failed
end
def failed!
@failed = true
end
# Register all provider instances with the class
#
# In order to flush all provider instances to a given file, we need to be
# able to track them all. When provider#flush is called and the file
# associated with that provider instance is dirty, the file needs to be
# flushed and all provider instances associated with that file will be
# passed to self.flush_file
def new(*args)
obj = super
@all_providers << obj
obj
end
# Returns all instances of the provider using this mixin.
#
# @return [Array<Puppet::Provider>]
def instances
provider_hashes = load_all_providers_from_disk
provider_hashes.map do |h|
h.merge!({:provider => self.name, :ensure => :present})
new(h)
end
rescue
# If something failed while loading instances, mark the provider class
# as failed and pass the exception along
@failed = true
raise
end
# Validate that the required methods are available.
#
# @raise Puppet::DevError if an expected method is unavailable
def validate_class!
required_class_hooks = [:target_files, :parse_file, :format_file]
required_instance_hooks = [:select_file]
required_class_hooks.each do |method|
raise Puppet::DevError, "#{self} has not implemented `self.#{method}`" unless self.respond_to? method
end
required_instance_hooks.each do |method|
raise Puppet::DevError, "#{self} has not implemented `##{method}`" unless self.method_defined? method
end
end
# Reads all files from disk and returns an array of hashes representing
# provider instances.
#
# @return [Array<Hash<String, Hash<Symbol, Object>>>]
# An array containing a set of hashes, keyed with a file path and values
# being a hash containg the state of the file and the filetype associated
# with it.
#
# @example
# IncludingProvider.load_all_providers_from_disk
# # => [
# # { "/path/to/file" => {
# # :dirty => false,
# # :filetype => #<Puppet::Util::FileTypeFlat:0x007fbf5b05ff10>,
# # },
# # { "/path/to/another/file" => {
# # :dirty => false,
# # :filetype => #<Puppet::Util::FileTypeFlat:0x007fbf5b05c108,
# # },
# #
#
def load_all_providers_from_disk
validate_class!
# Retrieve a list of files to fetch, and cache a copy of a filetype
# for each one
target_files.each do |file|
@mapped_files[file][:filetype] = Puppet::Util::FileType.filetype(self.filetype).new(file)
@mapped_files[file][:dirty] = false
end
# Read and parse each file.
provider_hashes = []
@mapped_files.each_pair do |filename, file_attrs|
arr = parse_file(filename, file_attrs[:filetype].read)
unless arr.is_a? Array
raise Puppet::DevError, "expected #{self}.parse_file to return an Array, got a #{arr.class}"
end
provider_hashes.concat arr
end
provider_hashes
end
# Match up all resources that have existing providers.
#
# Pass over all provider instances, and see if there is a resource with the
# same namevar as a provider instance. If such a resource exists, set the
# provider field of that resource to the existing provider.
#
# This is a hook method that will be called by Puppet::Transaction#prefetch
#
# @param [Hash<String, Puppet::Resource>] resources
def prefetch(resources = {})
# generate hash of {provider_name => provider}
providers = instances.inject({}) do |hash, instance|
hash[instance.name] = instance
hash
end
# For each prefetched resource, try to match it to a provider
resources.each_pair do |resource_name, resource|
if provider = providers[resource_name]
resource.provider = provider
end
end
end
# Create attr_accessors for properties and mark the provider as dirty on change.
def mk_property_methods
resource_type.validproperties.each do |attr|
attr = attr.intern if attr.respond_to? :intern and not attr.is_a? Symbol
# Generate the attr_reader method
define_method(attr) do
@property_hash[attr]
end
# Generate the attr_writer and have it mark the resource as dirty when called
define_method("#{attr}=") do |val|
@property_hash[attr] = val
self.dirty!
end
end
end
# Generate an array of providers that should be flushed to a specific file
#
# Only providers that should be present will be returned regardless of
# the containing file.
#
# @param [String] filename The name of the file to find providers for
#
# @return [Array<Puppet::Provider>]
def collect_providers_for_file(filename)
@all_providers.select do |provider|
provider.select_file == filename and provider.ensure == :present
end
end
def dirty_file!(filename)
@mapped_files[filename][:dirty] = true
end
# Flush provider instances associated with the given file and call any defined hooks
#
# If the provider is in a failure state, the provider class will refuse to
# flush any file, since we're in an unknown state.
#
# This method respects two method hooks: `pre_flush_hook` and `post_flush_hook`.
# These methods must accept one argument, the path of the file being flushed.
# `post_flush_hook` is guaranteed to be called after the flush has occurred.
#
# @param [String] filename The path of the file to be flushed
def flush_file(filename)
if failed?
err "#{self.name} is in an error state, refusing to flush file #{filename}"
return
end
if not @mapped_files[filename][:dirty]
Puppet.debug "#{self.name} was requested to flush the file #{filename}, but it was not marked as dirty - doing nothing."
else
# Collect all providers that should be present and pass them to the
# including class for formatting.
target_providers = collect_providers_for_file(filename)
file_contents = self.format_file(filename, target_providers)
unless file_contents.is_a? String
raise Puppet::DevError, "expected #{self}.format_file to return a String, got a #{file_contents.class}"
end
# Call the `pre_flush_hook` method if it's defined
pre_flush_hook(filename) if self.respond_to? :pre_flush_hook
begin
if file_contents.empty? and self.unlink_empty_files
remove_empty_file(filename)
else
perform_write(filename, file_contents)
end
ensure
post_flush_hook(filename) if self.respond_to? :post_flush_hook
end
end
rescue => e
# If something failed during the flush process, mark the provider as
# failed. There's not much we can do about any file that's already been
# flushed but we can stop smashing things.
@failed = true
raise
end
# We have a dirty file and the new contents ready, back up the file and perform the flush.
#
# @param [String] filename The destination filename
# @param [String] contents The new file contents
def perform_write(filename, contents)
@mapped_files[filename][:filetype] ||= Puppet::Util::FileType.filetype(self.filetype).new(filename)
filetype = @mapped_files[filename][:filetype]
filetype.backup if filetype.respond_to? :backup
filetype.write(contents)
end
# Back up and remove a file, if it exists
#
# @param [String] filename The file to remove
def remove_empty_file(filename)
if File.exist? filename
@mapped_files[filename][:filetype] ||= Puppet::Util::FileType.filetype(self.filetype).new(filename)
filetype = @mapped_files[filename][:filetype]
filetype.backup if filetype.respond_to? :backup
File.unlink(filename)
end
end
end
end