filemapper.rb 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. require 'puppet/util/filetype'
  2. # Forward declaration
  3. module PuppetX; end
  4. module PuppetX::FileMapper
  5. # Copy all desired resource properties into this resource for generation upon flush
  6. #
  7. # This method is necessary for the provider to be ensurable
  8. def create
  9. raise Puppet::Error, "#{self.class} is in an error state" if self.class.failed?
  10. @resource.class.validproperties.each do |property|
  11. if value = @resource.should(property)
  12. @property_hash[property] = value
  13. end
  14. end
  15. self.dirty!
  16. end
  17. # Use the prefetched status to determine of the resource exists.
  18. #
  19. # This method is necessary for the provider to be ensurable
  20. #
  21. # @return [TrueClass || FalseClass]
  22. def exists?
  23. @property_hash[:ensure] and @property_hash[:ensure] == :present
  24. end
  25. # Update the property hash to mark this resource as absent for flushing
  26. #
  27. # This method is necessary for the provider to be ensurable
  28. def destroy
  29. @property_hash[:ensure] = :absent
  30. self.dirty!
  31. end
  32. # Mark the file associated with this resource as dirty
  33. def dirty!
  34. file = select_file
  35. self.class.dirty_file! file
  36. end
  37. # When processing on this resource is complete, trigger a flush on the file
  38. # that this resource belongs to.
  39. def flush
  40. self.class.flush_file(self.select_file)
  41. end
  42. def self.included(klass)
  43. klass.extend PuppetX::FileMapper::ClassMethods
  44. klass.mk_property_methods
  45. klass.initvars
  46. end
  47. module ClassMethods
  48. # @!attribute [rw] unlink_empty_files
  49. # @return [TrueClass || FalseClass] Whether empty files will be removed
  50. attr_accessor :unlink_empty_files
  51. # @!attribute [r] mapped_files
  52. # @return [Hash<filepath => Hash<:dirty => Bool, :filetype => Filetype>>]
  53. # A data structure representing the file paths and filetypes backing this
  54. # provider.
  55. attr_reader :mapped_files
  56. def initvars
  57. @mapped_files = Hash.new {|h, k| h[k] = {}}
  58. @failed = false
  59. @all_providers = []
  60. end
  61. def failed?
  62. @failed
  63. end
  64. def failed!
  65. @failed = true
  66. end
  67. # Register all provider instances with the class
  68. #
  69. # In order to flush all provider instances to a given file, we need to be
  70. # able to track them all. When provider#flush is called and the file
  71. # associated with that provider instance is dirty, the file needs to be
  72. # flushed and all provider instances associated with that file will be
  73. # passed to self.flush_file
  74. def new(*args)
  75. obj = super
  76. @all_providers << obj
  77. obj
  78. end
  79. # Returns all instances of the provider using this mixin.
  80. #
  81. # @return [Array<Puppet::Provider>]
  82. def instances
  83. provider_hashes = load_all_providers_from_disk
  84. provider_hashes.map do |h|
  85. h.merge!({:provider => self.name, :ensure => :present})
  86. new(h)
  87. end
  88. rescue
  89. # If something failed while loading instances, mark the provider class
  90. # as failed and pass the exception along
  91. @failed = true
  92. raise
  93. end
  94. # Validate that the required methods are available.
  95. #
  96. # @raise Puppet::DevError if an expected method is unavailable
  97. def validate_class!
  98. required_class_hooks = [:target_files, :parse_file, :format_file]
  99. required_instance_hooks = [:select_file]
  100. required_class_hooks.each do |method|
  101. raise Puppet::DevError, "#{self} has not implemented `self.#{method}`" unless self.respond_to? method
  102. end
  103. required_instance_hooks.each do |method|
  104. raise Puppet::DevError, "#{self} has not implemented `##{method}`" unless self.method_defined? method
  105. end
  106. end
  107. # Reads all files from disk and returns an array of hashes representing
  108. # provider instances.
  109. #
  110. # @return [Array<Hash<String, Hash<Symbol, Object>>>]
  111. # An array containing a set of hashes, keyed with a file path and values
  112. # being a hash containg the state of the file and the filetype associated
  113. # with it.
  114. #
  115. # @example
  116. # IncludingProvider.load_all_providers_from_disk
  117. # # => [
  118. # # { "/path/to/file" => {
  119. # # :dirty => false,
  120. # # :filetype => #<Puppet::Util::FileTypeFlat:0x007fbf5b05ff10>,
  121. # # },
  122. # # { "/path/to/another/file" => {
  123. # # :dirty => false,
  124. # # :filetype => #<Puppet::Util::FileTypeFlat:0x007fbf5b05c108,
  125. # # },
  126. # #
  127. #
  128. def load_all_providers_from_disk
  129. validate_class!
  130. # Retrieve a list of files to fetch, and cache a copy of a filetype
  131. # for each one
  132. target_files.each do |file|
  133. @mapped_files[file][:filetype] = Puppet::Util::FileType.filetype(:flat).new(file)
  134. @mapped_files[file][:dirty] = false
  135. end
  136. # Read and parse each file.
  137. provider_hashes = []
  138. @mapped_files.each_pair do |filename, file_attrs|
  139. arr = parse_file(filename, file_attrs[:filetype].read)
  140. unless arr.is_a? Array
  141. raise Puppet::DevError, "expected #{self}.parse_file to return an Array, got a #{arr.class}"
  142. end
  143. provider_hashes.concat arr
  144. end
  145. provider_hashes
  146. end
  147. # Match up all resources that have existing providers.
  148. #
  149. # Pass over all provider instances, and see if there is a resource with the
  150. # same namevar as a provider instance. If such a resource exists, set the
  151. # provider field of that resource to the existing provider.
  152. #
  153. # This is a hook method that will be called by Puppet::Transaction#prefetch
  154. #
  155. # @param [Hash<String, Puppet::Resource>] resources
  156. def prefetch(resources = {})
  157. # generate hash of {provider_name => provider}
  158. providers = instances.inject({}) do |hash, instance|
  159. hash[instance.name] = instance
  160. hash
  161. end
  162. # For each prefetched resource, try to match it to a provider
  163. resources.each_pair do |resource_name, resource|
  164. if provider = providers[resource_name]
  165. resource.provider = provider
  166. end
  167. end
  168. end
  169. # Create attr_accessors for properties and mark the provider as dirty on change.
  170. def mk_property_methods
  171. resource_type.validproperties.each do |attr|
  172. attr = attr.intern if attr.respond_to? :intern and not attr.is_a? Symbol
  173. # Generate the attr_reader method
  174. define_method(attr) do
  175. @property_hash[attr]
  176. end
  177. # Generate the attr_writer and have it mark the resource as dirty when called
  178. define_method("#{attr}=") do |val|
  179. @property_hash[attr] = val
  180. self.dirty!
  181. end
  182. end
  183. end
  184. # Generate an array of providers that should be flushed to a specific file
  185. #
  186. # Only providers that should be present will be returned regardless of
  187. # the containing file.
  188. #
  189. # @param [String] filename The name of the file to find providers for
  190. #
  191. # @return [Array<Puppet::Provider>]
  192. def collect_providers_for_file(filename)
  193. @all_providers.select do |provider|
  194. provider.select_file == filename and provider.ensure == :present
  195. end
  196. end
  197. def dirty_file!(filename)
  198. @mapped_files[filename][:dirty] = true
  199. end
  200. # Flush provider instances associated with the given file and call any defined hooks
  201. #
  202. # If the provider is in a failure state, the provider class will refuse to
  203. # flush any file, since we're in an unknown state.
  204. #
  205. # This method respects two method hooks: `pre_flush_hook` and `post_flush_hook`.
  206. # These methods must accept one argument, the path of the file being flushed.
  207. # `post_flush_hook` is guaranteed to be called after the flush has occurred.
  208. #
  209. # @param [String] filename The path of the file to be flushed
  210. def flush_file(filename)
  211. if failed?
  212. err "#{self.name} is in an error state, refusing to flush file #{filename}"
  213. return
  214. end
  215. if not @mapped_files[filename][:dirty]
  216. Puppet.debug "#{self.name} was requested to flush the file #{filename}, but it was not marked as dirty - doing nothing."
  217. else
  218. # Collect all providers that should be present and pass them to the
  219. # including class for formatting.
  220. target_providers = collect_providers_for_file(filename)
  221. file_contents = self.format_file(filename, target_providers)
  222. unless file_contents.is_a? String
  223. raise Puppet::DevError, "expected #{self}.format_file to return a String, got a #{file_contents.class}"
  224. end
  225. # Call the `pre_flush_hook` method if it's defined
  226. pre_flush_hook(filename) if self.respond_to? :pre_flush_hook
  227. begin
  228. if file_contents.empty? and self.unlink_empty_files
  229. remove_empty_file(filename)
  230. else
  231. perform_write(filename, file_contents)
  232. end
  233. ensure
  234. post_flush_hook(filename) if self.respond_to? :post_flush_hook
  235. end
  236. end
  237. rescue => e
  238. # If something failed during the flush process, mark the provider as
  239. # failed. There's not much we can do about any file that's already been
  240. # flushed but we can stop smashing things.
  241. @failed = true
  242. raise
  243. end
  244. # We have a dirty file and the new contents ready, back up the file and perform the flush.
  245. #
  246. # @param [String] filename The destination filename
  247. # @param [String] contents The new file contents
  248. def perform_write(filename, contents)
  249. @mapped_files[filename][:filetype] ||= Puppet::Util::FileType.filetype(:flat).new(filename)
  250. filetype = @mapped_files[filename][:filetype]
  251. filetype.backup
  252. filetype.write(contents)
  253. end
  254. # Back up and remove a file, if it exists
  255. #
  256. # @param [String] filename The file to remove
  257. def remove_empty_file(filename)
  258. if File.exist? filename
  259. @mapped_files[filename][:filetype] ||= Puppet::Util::FileType.filetype(:flat).new(filename)
  260. filetype = @mapped_files[filename][:filetype]
  261. filetype.backup
  262. File.unlink(filename)
  263. end
  264. end
  265. end
  266. end