class Puppet::Provider::ParsedFile
This provider can be used as the parent class for a provider that parses and generates files. Its content must be loaded via the 'prefetch' method, and the file will be written when 'flush' is called on the provider instance. At this point, the file is written once for every provider instance.
Once the provider prefetches the data, it's the resource's job to copy that data over to the @is variables.
NOTE: The prefetch method swallows FileReadErrors by treating the corresponding target as an empty file. If you would like to turn this behavior off, then set the raise_prefetch_errors
class variable to true. Doing so will error all resources associated with the failed target.
Attributes
Public Class Methods
Make sure our file is backed up, but only back it up once per transaction. We cheat and rely on the fact that @records is created on each prefetch.
# File lib/puppet/provider/parsedfile.rb 89 def self.backup_target(target) 90 return nil unless target_object(target).respond_to?(:backup) 91 92 @backup_stats ||= {} 93 return nil if @backup_stats[target] == @records.object_id 94 95 target_object(target).backup 96 @backup_stats[target] = @records.object_id 97 end
# File lib/puppet/provider/parsedfile.rb 28 def self.clean(hash) 29 newhash = hash.dup 30 [:record_type, :on_disk].each do |p| 31 newhash.delete(p) if newhash.include?(p) 32 end 33 34 newhash 35 end
# File lib/puppet/provider/parsedfile.rb 37 def self.clear 38 @target_objects.clear 39 @records.clear 40 end
The mode for generated files if they are newly created. No mode will be set on existing files.
@abstract Providers inheriting parsedfile can override this method
to provide a mode. The value should be suitable for File.chmod
# File lib/puppet/provider/parsedfile.rb 353 def self.default_mode 354 nil 355 end
How to handle third party headers. @api private @abstract Providers based on ParsedFile
that make use of the support for
third party headers may override this method to return +true+. When this is done, headers that are matched by the native_header_regex are not written back to disk.
@see native_header_regex
# File lib/puppet/provider/parsedfile.rb 149 def self.drop_native_header 150 false 151 end
# File lib/puppet/provider/parsedfile.rb 42 def self.filetype 43 @filetype ||= Puppet::Util::FileType.filetype(:flat) 44 end
# File lib/puppet/provider/parsedfile.rb 46 def self.filetype=(type) 47 if type.is_a?(Class) 48 @filetype = type 49 else 50 klass = Puppet::Util::FileType.filetype(type) 51 if klass 52 @filetype = klass 53 else 54 raise ArgumentError, _("Invalid filetype %{type}") % { type: type } 55 end 56 end 57 end
Flush all of the targets for which there are modified records. The only reason we pass a record here is so that we can add it to the stack if necessary – it's passed from the instance calling 'flush'.
# File lib/puppet/provider/parsedfile.rb 62 def self.flush(record) 63 # Make sure this record is on the list to be flushed. 64 unless record[:on_disk] 65 record[:on_disk] = true 66 @records << record 67 68 # If we've just added the record, then make sure our 69 # target will get flushed. 70 modified(record[:target] || default_target) 71 end 72 73 return unless defined?(@modified) and ! @modified.empty? 74 75 flushed = [] 76 begin 77 @modified.sort_by(&:to_s).uniq.each do |target| 78 Puppet.debug "Flushing #{@resource_type.name} provider target #{target}" 79 flushed << target 80 flush_target(target) 81 end 82 ensure 83 @modified.reject! { |t| flushed.include?(t) } 84 end 85 end
Flush all of the records relating to a specific target.
# File lib/puppet/provider/parsedfile.rb 100 def self.flush_target(target) 101 if @raise_prefetch_errors && @failed_prefetch_targets.key?(target) 102 raise Puppet::Error, _("Failed to read %{target}'s records when prefetching them. Reason: %{detail}") % { target: target, detail: @failed_prefetch_targets[target] } 103 end 104 105 backup_target(target) 106 107 records = target_records(target).reject { |r| 108 r[:ensure] == :absent 109 } 110 111 target_object(target).write(to_file(records)) 112 end
Return the header placed at the top of each generated file, warning users that modifying this file manually is probably a bad idea.
# File lib/puppet/provider/parsedfile.rb 116 def self.header 117 %{# HEADER: This file was autogenerated at #{Time.now} 118 # HEADER: by puppet. While it can still be managed manually, it 119 # HEADER: is definitely not recommended.\n} 120 end
Add another type var.
Puppet::Provider::initvars
# File lib/puppet/provider/parsedfile.rb 154 def self.initvars 155 @records = [] 156 @target_objects = {} 157 158 # Hash of <target> => <failure reason>. 159 @failed_prefetch_targets = {} 160 @raise_prefetch_errors = false 161 162 @target = nil 163 164 # Default to flat files 165 @filetype ||= Puppet::Util::FileType.filetype(:flat) 166 super 167 end
Return a list of all of the records we can find.
# File lib/puppet/provider/parsedfile.rb 170 def self.instances 171 targets.collect do |target| 172 prefetch_target(target) 173 end.flatten.reject { |r| skip_record?(r) }.collect do |record| 174 new(record) 175 end 176 end
Match a list of catalog resources with provider instances
@api private
@param [Array<Puppet::Resource>] resources A list of resources using this class as a provider
# File lib/puppet/provider/parsedfile.rb 236 def self.match_providers_with_resources(resources) 237 return unless resources 238 matchers = resources.dup 239 @records.each do |record| 240 # Skip things like comments and blank lines 241 next if skip_record?(record) 242 243 if (resource = resource_for_record(record, resources)) 244 resource.provider = new(record) 245 elsif respond_to?(:match) 246 resource = match(record, matchers) 247 if resource 248 matchers.delete(resource.title) 249 record[:name] = resource[:name] 250 resource.provider = new(record) 251 end 252 end 253 end 254 end
Override the default method with a lot more functionality.
# File lib/puppet/provider/parsedfile.rb 179 def self.mk_resource_methods 180 [resource_type.validproperties, resource_type.parameters].flatten.each do |attr| 181 attr = attr.intern 182 define_method(attr) do 183 # If it's not a valid field for this record type (which can happen 184 # when different platforms support different fields), then just 185 # return the should value, so the resource shuts up. 186 if @property_hash[attr] or self.class.valid_attr?(self.class.name, attr) 187 @property_hash[attr] || :absent 188 else 189 if defined?(@resource) 190 @resource.should(attr) 191 else 192 nil 193 end 194 end 195 end 196 197 define_method(attr.to_s + "=") do |val| 198 mark_target_modified 199 @property_hash[attr] = val 200 end 201 end 202 end
Mark a target as modified so we know to flush it. This only gets used within the attr= methods.
# File lib/puppet/provider/parsedfile.rb 212 def self.modified(target) 213 @modified ||= [] 214 @modified << target unless @modified.include?(target) 215 end
An optional regular expression matched by third party headers.
For example, this can be used to filter the vixie cron headers as erroneously exported by older cron versions.
@api private @abstract Providers based on ParsedFile
may implement this to make it
possible to identify a header maintained by a third party tool. The provider can then allow that header to remain near the top of the written file, or remove it after composing the file content. If implemented, the function must return a Regexp object. The expression must be tailored to match exactly one third party header.
@see drop_native_header
@note When specifying regular expressions in multiline mode, avoid
greedy repetitions such as '.*' (use .*? instead). Otherwise, the provider may drop file content between sparse headers.
# File lib/puppet/provider/parsedfile.rb 138 def self.native_header_regex 139 nil 140 end
Puppet::Provider::new
# File lib/puppet/provider/parsedfile.rb 461 def initialize(record) 462 super 463 464 # The 'record' could be a resource or a record, depending on how the provider 465 # is initialized. If we got an empty property hash (probably because the resource 466 # is just being initialized), then we want to set up some defaults. 467 @property_hash = self.class.record?(resource[:name]) || {:record_type => self.class.name, :ensure => :absent} if @property_hash.empty? 468 end
Retrieve all of the data from disk. There are three ways to know which files to retrieve: We might have a list of file objects already set up, there might be instances of our associated resource and they will have a path parameter set, and we will have a default path set. We need to turn those three locations into a list of files, prefetch each one, and make sure they're associated with each appropriate resource instance.
# File lib/puppet/provider/parsedfile.rb 224 def self.prefetch(resources = nil) 225 # Reset the record list. 226 @records = prefetch_all_targets(resources) 227 228 match_providers_with_resources(resources) 229 end
# File lib/puppet/provider/parsedfile.rb 271 def self.prefetch_all_targets(resources) 272 records = [] 273 targets(resources).each do |target| 274 records += prefetch_target(target) 275 end 276 records 277 end
Prefetch an individual target.
# File lib/puppet/provider/parsedfile.rb 280 def self.prefetch_target(target) 281 begin 282 target_records = retrieve(target) 283 unless target_records 284 raise Puppet::DevError, _("Prefetching %{target} for provider %{name} returned nil") % { target: target, name: self.name } 285 end 286 rescue Puppet::Util::FileType::FileReadError => detail 287 if @raise_prefetch_errors 288 # We will raise an error later in flush_target. This way, 289 # only the resources linked to our target will fail 290 # evaluation. 291 @failed_prefetch_targets[target] = detail.to_s 292 else 293 puts detail.backtrace if Puppet[:trace] 294 Puppet.err _("Could not prefetch %{resource} provider '%{name}' target '%{target}': %{detail}. Treating as empty") % { resource: self.resource_type.name, name: self.name, target: target, detail: detail } 295 end 296 297 target_records = [] 298 end 299 300 target_records.each do |r| 301 r[:on_disk] = true 302 r[:target] = target 303 r[:ensure] = :present 304 end 305 306 target_records = prefetch_hook(target_records) if respond_to?(:prefetch_hook) 307 308 raise Puppet::DevError, _("Prefetching %{target} for provider %{name} returned nil") % { target: target, name: self.name } unless target_records 309 310 target_records 311 end
Is there an existing record with this name?
# File lib/puppet/provider/parsedfile.rb 314 def self.record?(name) 315 return nil unless @records 316 @records.find { |r| r[:name] == name } 317 end
Look up a resource based on a parsed file record
@api private
@param [Hash<Symbol, Object>] record @param [Array<Puppet::Resource>] resources
@return [Puppet::Resource, nil] The resource if found, else nil
# File lib/puppet/provider/parsedfile.rb 264 def self.resource_for_record(record, resources) 265 name = record[:name] 266 if name 267 resources[name] 268 end 269 end
Always make the resource methods.
# File lib/puppet/provider/parsedfile.rb 205 def self.resource_type=(resource) 206 super 207 mk_resource_methods 208 end
Retrieve the text for the file. Returns nil in the unlikely event that it doesn't exist.
# File lib/puppet/provider/parsedfile.rb 321 def self.retrieve(path) 322 # XXX We need to be doing something special here in case of failure. 323 text = target_object(path).read 324 if text.nil? or text == "" 325 # there is no file 326 return [] 327 else 328 # Set the target, for logging. 329 old = @target 330 begin 331 @target = path 332 return self.parse(text) 333 rescue Puppet::Error => detail 334 detail.file = @target if detail.respond_to?(:file=) 335 raise detail 336 ensure 337 @target = old 338 end 339 end 340 end
Should we skip the record? Basically, we skip text records. This is only here so subclasses can override it.
# File lib/puppet/provider/parsedfile.rb 344 def self.skip_record?(record) 345 record_type(record[:record_type]).text? 346 end
Initialize the object if necessary.
# File lib/puppet/provider/parsedfile.rb 358 def self.target_object(target) 359 # only send the default mode if the actual provider defined it, 360 # because certain filetypes (e.g. the crontab variants) do not 361 # expect it in their initialize method 362 if default_mode 363 @target_objects[target] ||= filetype.new(target, default_mode) 364 else 365 @target_objects[target] ||= filetype.new(target) 366 end 367 368 @target_objects[target] 369 end
Find all of the records for a given target
# File lib/puppet/provider/parsedfile.rb 372 def self.target_records(target) 373 @records.find_all { |r| r[:target] == target } 374 end
Find a list of all of the targets that we should be reading. This is used to figure out what targets we need to prefetch.
# File lib/puppet/provider/parsedfile.rb 378 def self.targets(resources = nil) 379 targets = [] 380 # First get the default target 381 raise Puppet::DevError, _("Parsed Providers must define a default target") unless self.default_target 382 targets << self.default_target 383 384 # Then get each of the file objects 385 targets += @target_objects.keys 386 387 # Lastly, check the file from any resource instances 388 if resources 389 resources.each do |name, resource| 390 value = resource.should(:target) 391 if value 392 targets << value 393 end 394 end 395 end 396 397 targets.uniq.compact 398 end
Compose file contents from the set of records.
If self.native_header_regex is not nil, possible vendor headers are identified by matching the return value against the expression. If one (or several consecutive) such headers, are found, they are either moved in front of the self.header if self.drop_native_header is false (this is the default), or removed from the return value otherwise.
@api private
# File lib/puppet/provider/parsedfile.rb 409 def self.to_file(records) 410 text = super 411 if native_header_regex and (match = text.match(native_header_regex)) 412 if drop_native_header 413 # concatenate the text in front of and after the native header 414 text = match.pre_match + match.post_match 415 else 416 native_header = match[0] 417 return native_header + header + match.pre_match + match.post_match 418 end 419 end 420 header + text 421 end
Public Instance Methods
# File lib/puppet/provider/parsedfile.rb 423 def create 424 @resource.class.validproperties.each do |property| 425 value = @resource.should(property) 426 if value 427 @property_hash[property] = value 428 end 429 end 430 mark_target_modified 431 (@resource.class.name.to_s + "_created").intern 432 end
# File lib/puppet/provider/parsedfile.rb 434 def destroy 435 # We use the method here so it marks the target as modified. 436 self.ensure = :absent 437 (@resource.class.name.to_s + "_deleted").intern 438 end
# File lib/puppet/provider/parsedfile.rb 440 def exists? 441 !(@property_hash[:ensure] == :absent or @property_hash[:ensure].nil?) 442 end
Write our data to disk.
# File lib/puppet/provider/parsedfile.rb 445 def flush 446 # Make sure we've got a target and name set. 447 448 # If the target isn't set, then this is our first modification, so 449 # mark it for flushing. 450 unless @property_hash[:target] 451 @property_hash[:target] = @resource.should(:target) || self.class.default_target 452 self.class.modified(@property_hash[:target]) 453 end 454 @resource.class.key_attributes.each do |attr| 455 @property_hash[attr] ||= @resource[attr] 456 end 457 458 self.class.flush(@property_hash) 459 end
Retrieve the current state from disk.
# File lib/puppet/provider/parsedfile.rb 471 def prefetch 472 raise Puppet::DevError, _("Somehow got told to prefetch with no resource set") unless @resource 473 self.class.prefetch(@resource[:name] => @resource) 474 end
# File lib/puppet/provider/parsedfile.rb 476 def record_type 477 @property_hash[:record_type] 478 end
Private Instance Methods
Mark both the resource and provider target as modified.
# File lib/puppet/provider/parsedfile.rb 483 def mark_target_modified 484 restarget = @resource.should(:target) if defined?(@resource) 485 if restarget && restarget != @property_hash[:target] 486 self.class.modified(restarget) 487 end 488 self.class.modified(@property_hash[:target]) if @property_hash[:target] != :absent and @property_hash[:target] 489 end