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 90 def self.backup_target(target) 91 return nil unless target_object(target).respond_to?(:backup) 92 93 @backup_stats ||= {} 94 return nil if @backup_stats[target] == @records.object_id 95 96 target_object(target).backup 97 @backup_stats[target] = @records.object_id 98 end
# File lib/puppet/provider/parsedfile.rb 29 def self.clean(hash) 30 newhash = hash.dup 31 [:record_type, :on_disk].each do |p| 32 newhash.delete(p) if newhash.include?(p) 33 end 34 35 newhash 36 end
# File lib/puppet/provider/parsedfile.rb 38 def self.clear 39 @target_objects.clear 40 @records.clear 41 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 354 def self.default_mode 355 nil 356 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 150 def self.drop_native_header 151 false 152 end
# File lib/puppet/provider/parsedfile.rb 43 def self.filetype 44 @filetype ||= Puppet::Util::FileType.filetype(:flat) 45 end
# File lib/puppet/provider/parsedfile.rb 47 def self.filetype=(type) 48 if type.is_a?(Class) 49 @filetype = type 50 else 51 klass = Puppet::Util::FileType.filetype(type) 52 if klass 53 @filetype = klass 54 else 55 raise ArgumentError, _("Invalid filetype %{type}") % { type: type } 56 end 57 end 58 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 63 def self.flush(record) 64 # Make sure this record is on the list to be flushed. 65 unless record[:on_disk] 66 record[:on_disk] = true 67 @records << record 68 69 # If we've just added the record, then make sure our 70 # target will get flushed. 71 modified(record[:target] || default_target) 72 end 73 74 return unless defined?(@modified) and ! @modified.empty? 75 76 flushed = [] 77 begin 78 @modified.sort_by(&:to_s).uniq.each do |target| 79 Puppet.debug "Flushing #{@resource_type.name} provider target #{target}" 80 flushed << target 81 flush_target(target) 82 end 83 ensure 84 @modified.reject! { |t| flushed.include?(t) } 85 end 86 end
Flush all of the records relating to a specific target.
# File lib/puppet/provider/parsedfile.rb 101 def self.flush_target(target) 102 if @raise_prefetch_errors && @failed_prefetch_targets.key?(target) 103 raise Puppet::Error, _("Failed to read %{target}'s records when prefetching them. Reason: %{detail}") % { target: target, detail: @failed_prefetch_targets[target] } 104 end 105 106 backup_target(target) 107 108 records = target_records(target).reject { |r| 109 r[:ensure] == :absent 110 } 111 112 target_object(target).write(to_file(records)) 113 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 117 def self.header 118 %{# HEADER: This file was autogenerated at #{Time.now} 119 # HEADER: by puppet. While it can still be managed manually, it 120 # HEADER: is definitely not recommended.\n} 121 end
Add another type var.
Puppet::Provider::initvars
# File lib/puppet/provider/parsedfile.rb 155 def self.initvars 156 @records = [] 157 @target_objects = {} 158 159 # Hash of <target> => <failure reason>. 160 @failed_prefetch_targets = {} 161 @raise_prefetch_errors = false 162 163 @target = nil 164 165 # Default to flat files 166 @filetype ||= Puppet::Util::FileType.filetype(:flat) 167 super 168 end
Return a list of all of the records we can find.
# File lib/puppet/provider/parsedfile.rb 171 def self.instances 172 targets.collect do |target| 173 prefetch_target(target) 174 end.flatten.reject { |r| skip_record?(r) }.collect do |record| 175 new(record) 176 end 177 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 237 def self.match_providers_with_resources(resources) 238 return unless resources 239 matchers = resources.dup 240 @records.each do |record| 241 # Skip things like comments and blank lines 242 next if skip_record?(record) 243 244 if (resource = resource_for_record(record, resources)) 245 resource.provider = new(record) 246 elsif respond_to?(:match) 247 resource = match(record, matchers) 248 if resource 249 matchers.delete(resource.title) 250 record[:name] = resource[:name] 251 resource.provider = new(record) 252 end 253 end 254 end 255 end
Override the default method with a lot more functionality.
# File lib/puppet/provider/parsedfile.rb 180 def self.mk_resource_methods 181 [resource_type.validproperties, resource_type.parameters].flatten.each do |attr| 182 attr = attr.intern 183 define_method(attr) do 184 # If it's not a valid field for this record type (which can happen 185 # when different platforms support different fields), then just 186 # return the should value, so the resource shuts up. 187 if @property_hash[attr] or self.class.valid_attr?(self.class.name, attr) 188 @property_hash[attr] || :absent 189 else 190 if defined?(@resource) 191 @resource.should(attr) 192 else 193 nil 194 end 195 end 196 end 197 198 define_method(attr.to_s + "=") do |val| 199 mark_target_modified 200 @property_hash[attr] = val 201 end 202 end 203 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 213 def self.modified(target) 214 @modified ||= [] 215 @modified << target unless @modified.include?(target) 216 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 139 def self.native_header_regex 140 nil 141 end
Puppet::Provider::new
# File lib/puppet/provider/parsedfile.rb 462 def initialize(record) 463 super 464 465 # The 'record' could be a resource or a record, depending on how the provider 466 # is initialized. If we got an empty property hash (probably because the resource 467 # is just being initialized), then we want to set up some defaults. 468 @property_hash = self.class.record?(resource[:name]) || {:record_type => self.class.name, :ensure => :absent} if @property_hash.empty? 469 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 225 def self.prefetch(resources = nil) 226 # Reset the record list. 227 @records = prefetch_all_targets(resources) 228 229 match_providers_with_resources(resources) 230 end
# File lib/puppet/provider/parsedfile.rb 272 def self.prefetch_all_targets(resources) 273 records = [] 274 targets(resources).each do |target| 275 records += prefetch_target(target) 276 end 277 records 278 end
Prefetch an individual target.
# File lib/puppet/provider/parsedfile.rb 281 def self.prefetch_target(target) 282 begin 283 target_records = retrieve(target) 284 unless target_records 285 raise Puppet::DevError, _("Prefetching %{target} for provider %{name} returned nil") % { target: target, name: self.name } 286 end 287 rescue Puppet::Util::FileType::FileReadError => detail 288 if @raise_prefetch_errors 289 # We will raise an error later in flush_target. This way, 290 # only the resources linked to our target will fail 291 # evaluation. 292 @failed_prefetch_targets[target] = detail.to_s 293 else 294 puts detail.backtrace if Puppet[:trace] 295 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 } 296 end 297 298 target_records = [] 299 end 300 301 target_records.each do |r| 302 r[:on_disk] = true 303 r[:target] = target 304 r[:ensure] = :present 305 end 306 307 target_records = prefetch_hook(target_records) if respond_to?(:prefetch_hook) 308 309 raise Puppet::DevError, _("Prefetching %{target} for provider %{name} returned nil") % { target: target, name: self.name } unless target_records 310 311 target_records 312 end
Is there an existing record with this name?
# File lib/puppet/provider/parsedfile.rb 315 def self.record?(name) 316 return nil unless @records 317 @records.find { |r| r[:name] == name } 318 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 265 def self.resource_for_record(record, resources) 266 name = record[:name] 267 if name 268 resources[name] 269 end 270 end
Always make the resource methods.
# File lib/puppet/provider/parsedfile.rb 206 def self.resource_type=(resource) 207 super 208 mk_resource_methods 209 end
Retrieve the text for the file. Returns nil in the unlikely event that it doesn't exist.
# File lib/puppet/provider/parsedfile.rb 322 def self.retrieve(path) 323 # XXX We need to be doing something special here in case of failure. 324 text = target_object(path).read 325 if text.nil? or text == "" 326 # there is no file 327 return [] 328 else 329 # Set the target, for logging. 330 old = @target 331 begin 332 @target = path 333 return self.parse(text) 334 rescue Puppet::Error => detail 335 detail.file = @target if detail.respond_to?(:file=) 336 raise detail 337 ensure 338 @target = old 339 end 340 end 341 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 345 def self.skip_record?(record) 346 record_type(record[:record_type]).text? 347 end
Initialize the object if necessary.
# File lib/puppet/provider/parsedfile.rb 359 def self.target_object(target) 360 # only send the default mode if the actual provider defined it, 361 # because certain filetypes (e.g. the crontab variants) do not 362 # expect it in their initialize method 363 if default_mode 364 @target_objects[target] ||= filetype.new(target, default_mode) 365 else 366 @target_objects[target] ||= filetype.new(target) 367 end 368 369 @target_objects[target] 370 end
Find all of the records for a given target
# File lib/puppet/provider/parsedfile.rb 373 def self.target_records(target) 374 @records.find_all { |r| r[:target] == target } 375 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 379 def self.targets(resources = nil) 380 targets = [] 381 # First get the default target 382 raise Puppet::DevError, _("Parsed Providers must define a default target") unless self.default_target 383 targets << self.default_target 384 385 # Then get each of the file objects 386 targets += @target_objects.keys 387 388 # Lastly, check the file from any resource instances 389 if resources 390 resources.each do |name, resource| 391 value = resource.should(:target) 392 if value 393 targets << value 394 end 395 end 396 end 397 398 targets.uniq.compact 399 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 410 def self.to_file(records) 411 text = super 412 if native_header_regex and (match = text.match(native_header_regex)) 413 if drop_native_header 414 # concatenate the text in front of and after the native header 415 text = match.pre_match + match.post_match 416 else 417 native_header = match[0] 418 return native_header + header + match.pre_match + match.post_match 419 end 420 end 421 header + text 422 end
Public Instance Methods
# File lib/puppet/provider/parsedfile.rb 424 def create 425 @resource.class.validproperties.each do |property| 426 value = @resource.should(property) 427 if value 428 @property_hash[property] = value 429 end 430 end 431 mark_target_modified 432 (@resource.class.name.to_s + "_created").intern 433 end
# File lib/puppet/provider/parsedfile.rb 435 def destroy 436 # We use the method here so it marks the target as modified. 437 self.ensure = :absent 438 (@resource.class.name.to_s + "_deleted").intern 439 end
# File lib/puppet/provider/parsedfile.rb 441 def exists? 442 !(@property_hash[:ensure] == :absent or @property_hash[:ensure].nil?) 443 end
Write our data to disk.
# File lib/puppet/provider/parsedfile.rb 446 def flush 447 # Make sure we've got a target and name set. 448 449 # If the target isn't set, then this is our first modification, so 450 # mark it for flushing. 451 unless @property_hash[:target] 452 @property_hash[:target] = @resource.should(:target) || self.class.default_target 453 self.class.modified(@property_hash[:target]) 454 end 455 @resource.class.key_attributes.each do |attr| 456 @property_hash[attr] ||= @resource[attr] 457 end 458 459 self.class.flush(@property_hash) 460 end
Retrieve the current state from disk.
# File lib/puppet/provider/parsedfile.rb 472 def prefetch 473 raise Puppet::DevError, _("Somehow got told to prefetch with no resource set") unless @resource 474 self.class.prefetch(@resource[:name] => @resource) 475 end
# File lib/puppet/provider/parsedfile.rb 477 def record_type 478 @property_hash[:record_type] 479 end
Private Instance Methods
Mark both the resource and provider target as modified.
# File lib/puppet/provider/parsedfile.rb 484 def mark_target_modified 485 restarget = @resource.should(:target) if defined?(@resource) 486 if restarget && restarget != @property_hash[:target] 487 self.class.modified(restarget) 488 end 489 self.class.modified(@property_hash[:target]) if @property_hash[:target] != :absent and @property_hash[:target] 490 end