| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- 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 [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] = {}}
- @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(:flat).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(:flat).new(filename)
- filetype = @mapped_files[filename][:filetype]
- filetype.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(:flat).new(filename)
- filetype = @mapped_files[filename][:filetype]
- filetype.backup
- File.unlink(filename)
- end
- end
- end
- end
|