require 'puppet'
require 'puppet/util/filetype'
require 'puppet/util/fileparsing'
# 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.
class Puppet::Provider::ParsedFile < Puppet::Provider
extend Puppet::Util::FileParsing
class << self
attr_accessor :default_target, :target, :raise_prefetch_errors
end
attr_accessor :property_hash
def self.clean(hash)
newhash = hash.dup
[:record_type, :on_disk].each do |p|
newhash.delete(p) if newhash.include?(p)
end
newhash
end
def self.clear
@target_objects.clear
@records.clear
end
def self.filetype
@filetype ||= Puppet::Util::FileType.filetype(:flat)
end
def self.filetype=(type)
if type.is_a?(Class)
@filetype = type
else
klass = Puppet::Util::FileType.filetype(type)
if klass
@filetype = klass
else
raise ArgumentError, _("Invalid filetype %{type}") % { type: type }
end
end
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'.
def self.flush(record)
# Make sure this record is on the list to be flushed.
unless record[:on_disk]
record[:on_disk] = true
@records << record
# If we've just added the record, then make sure our
# target will get flushed.
modified(record[:target] || default_target)
end
return unless defined?(@modified) and ! @modified.empty?
flushed = []
begin
@modified.sort_by(&:to_s).uniq.each do |target|
Puppet.debug "Flushing #{@resource_type.name} provider target #{target}"
flushed << target
flush_target(target)
end
ensure
@modified.reject! { |t| flushed.include?(t) }
end
end
# 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.
def self.backup_target(target)
return nil unless target_object(target).respond_to?(:backup)
@backup_stats ||= {}
return nil if @backup_stats[target] == @records.object_id
target_object(target).backup
@backup_stats[target] = @records.object_id
end
# Flush all of the records relating to a specific target.
def self.flush_target(target)
if @raise_prefetch_errors && @failed_prefetch_targets.key?(target)
raise Puppet::Error, _("Failed to read %{target}'s records when prefetching them. Reason: %{detail}") % { target: target, detail: @failed_prefetch_targets[target] }
end
backup_target(target)
records = target_records(target).reject { |r|
r[:ensure] == :absent
}
target_object(target).write(to_file(records))
end
# Return the header placed at the top of each generated file, warning
# users that modifying this file manually is probably a bad idea.
def self.header
%{# HEADER: This file was autogenerated at #{Time.now}
# HEADER: by puppet. While it can still be managed manually, it
# HEADER: is definitely not recommended.\n}
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.
def self.native_header_regex
nil
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
def self.drop_native_header
false
end
# Add another type var.
def self.initvars
@records = []
@target_objects = {}
# Hash of <target> => <failure reason>.
@failed_prefetch_targets = {}
@raise_prefetch_errors = false
@target = nil
# Default to flat files
@filetype ||= Puppet::Util::FileType.filetype(:flat)
super
end
# Return a list of all of the records we can find.
def self.instances
targets.collect do |target|
prefetch_target(target)
end.flatten.reject { |r| skip_record?(r) }.collect do |record|
new(record)
end
end
# Override the default method with a lot more functionality.
def self.mk_resource_methods
[resource_type.validproperties, resource_type.parameters].flatten.each do |attr|
attr = attr.intern
define_method(attr) do
# If it's not a valid field for this record type (which can happen
# when different platforms support different fields), then just
# return the should value, so the resource shuts up.
if @property_hash[attr] or self.class.valid_attr?(self.class.name, attr)
@property_hash[attr] || :absent
else
if defined?(@resource)
@resource.should(attr)
else
nil
end
end
end
define_method(attr.to_s + "=") do |val|
mark_target_modified
@property_hash[attr] = val
end
end
end
# Always make the resource methods.
def self.resource_type=(resource)
super
mk_resource_methods
end
# Mark a target as modified so we know to flush it. This only gets
# used within the attr= methods.
def self.modified(target)
@modified ||= []
@modified << target unless @modified.include?(target)
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.
def self.prefetch(resources = nil)
# Reset the record list.
@records = prefetch_all_targets(resources)
match_providers_with_resources(resources)
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
def self.match_providers_with_resources(resources)
return unless resources
matchers = resources.dup
@records.each do |record|
# Skip things like comments and blank lines
next if skip_record?(record)
if (resource = resource_for_record(record, resources))
resource.provider = new(record)
elsif respond_to?(:match)
resource = match(record, matchers)
if resource
matchers.delete(resource.title)
record[:name] = resource[:name]
resource.provider = new(record)
end
end
end
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
def self.resource_for_record(record, resources)
name = record[:name]
if name
resources[name]
end
end
def self.prefetch_all_targets(resources)
records = []
targets(resources).each do |target|
records += prefetch_target(target)
end
records
end
# Prefetch an individual target.
def self.prefetch_target(target)
begin
target_records = retrieve(target)
unless target_records
raise Puppet::DevError, _("Prefetching %{target} for provider %{name} returned nil") % { target: target, name: self.name }
end
rescue Puppet::Util::FileType::FileReadError => detail
if @raise_prefetch_errors
# We will raise an error later in flush_target. This way,
# only the resources linked to our target will fail
# evaluation.
@failed_prefetch_targets[target] = detail.to_s
else
puts detail.backtrace if Puppet[:trace]
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 }
end
target_records = []
end
target_records.each do |r|
r[:on_disk] = true
r[:target] = target
r[:ensure] = :present
end
target_records = prefetch_hook(target_records) if respond_to?(:prefetch_hook)
raise Puppet::DevError, _("Prefetching %{target} for provider %{name} returned nil") % { target: target, name: self.name } unless target_records
target_records
end
# Is there an existing record with this name?
def self.record?(name)
return nil unless @records
@records.find { |r| r[:name] == name }
end
# Retrieve the text for the file. Returns nil in the unlikely
# event that it doesn't exist.
def self.retrieve(path)
# XXX We need to be doing something special here in case of failure.
text = target_object(path).read
if text.nil? or text == ""
# there is no file
return []
else
# Set the target, for logging.
old = @target
begin
@target = path
return self.parse(text)
rescue Puppet::Error => detail
detail.file = @target if detail.respond_to?(:file=)
raise detail
ensure
@target = old
end
end
end
# Should we skip the record? Basically, we skip text records.
# This is only here so subclasses can override it.
def self.skip_record?(record)
record_type(record[:record_type]).text?
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
def self.default_mode
nil
end
# Initialize the object if necessary.
def self.target_object(target)
# only send the default mode if the actual provider defined it,
# because certain filetypes (e.g. the crontab variants) do not
# expect it in their initialize method
if default_mode
@target_objects[target] ||= filetype.new(target, default_mode)
else
@target_objects[target] ||= filetype.new(target)
end
@target_objects[target]
end
# Find all of the records for a given target
def self.target_records(target)
@records.find_all { |r| r[:target] == target }
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.
def self.targets(resources = nil)
targets = []
# First get the default target
raise Puppet::DevError, _("Parsed Providers must define a default target") unless self.default_target
targets << self.default_target
# Then get each of the file objects
targets += @target_objects.keys
# Lastly, check the file from any resource instances
if resources
resources.each do |name, resource|
value = resource.should(:target)
if value
targets << value
end
end
end
targets.uniq.compact
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
def self.to_file(records)
text = super
if native_header_regex and (match = text.match(native_header_regex))
if drop_native_header
# concatenate the text in front of and after the native header
text = match.pre_match + match.post_match
else
native_header = match[0]
return native_header + header + match.pre_match + match.post_match
end
end
header + text
end
def create
@resource.class.validproperties.each do |property|
value = @resource.should(property)
if value
@property_hash[property] = value
end
end
mark_target_modified
(@resource.class.name.to_s + "_created").intern
end
def destroy
# We use the method here so it marks the target as modified.
self.ensure = :absent
(@resource.class.name.to_s + "_deleted").intern
end
def exists?
!(@property_hash[:ensure] == :absent or @property_hash[:ensure].nil?)
end
# Write our data to disk.
def flush
# Make sure we've got a target and name set.
# If the target isn't set, then this is our first modification, so
# mark it for flushing.
unless @property_hash[:target]
@property_hash[:target] = @resource.should(:target) || self.class.default_target
self.class.modified(@property_hash[:target])
end
@resource.class.key_attributes.each do |attr|
@property_hash[attr] ||= @resource[attr]
end
self.class.flush(@property_hash)
end
def initialize(record)
super
# The 'record' could be a resource or a record, depending on how the provider
# is initialized. If we got an empty property hash (probably because the resource
# is just being initialized), then we want to set up some defaults.
@property_hash = self.class.record?(resource[:name]) || {:record_type => self.class.name, :ensure => :absent} if @property_hash.empty?
end
# Retrieve the current state from disk.
def prefetch
raise Puppet::DevError, _("Somehow got told to prefetch with no resource set") unless @resource
self.class.prefetch(@resource[:name] => @resource)
end
def record_type
@property_hash[:record_type]
end
private
# Mark both the resource and provider target as modified.
def mark_target_modified
restarget = @resource.should(:target) if defined?(@resource)
if restarget && restarget != @property_hash[:target]
self.class.modified(restarget)
end
self.class.modified(@property_hash[:target]) if @property_hash[:target] != :absent and @property_hash[:target]
end
end
Copyright 2K16 - 2K18 Indonesian Hacker Rulez