diff --git a/lib/puppet/indirector/file.rb b/lib/puppet/indirector/file.rb index 99d95ecb2..cc7ad9b07 100644 --- a/lib/puppet/indirector/file.rb +++ b/lib/puppet/indirector/file.rb @@ -1,58 +1,79 @@ require 'puppet/indirector/terminus' -# An empty terminus type, meant to just return empty objects. +# Store instances as files, usually serialized using some format. class Puppet::Indirector::File < Puppet::Indirector::Terminus + # Where do we store our data? + def data_directory + name = Puppet[:name] == "puppetmasterd" ? :server_datadir : :client_datadir + + File.join(Puppet.settings[name], self.class.indirection_name.to_s) + end + + def file_format(path) + path =~ /\.(\w+)$/ and return $1 + end + + def file_path(request) + File.join(data_directory, request.key + "." + serialization_format) + end + + def latest_path(request) + files = Dir.glob(File.join(data_directory, request.key + ".*")) + return nil if files.empty? + + # Return the newest file. + files.sort { |a, b| File.stat(b).mtime <=> File.stat(a).mtime }[0] + end + + def serialization_format + model.default_format + end + # Remove files on disk. def destroy(request) - if respond_to?(:path) - path = path(request.key) - else - path = request.key - end - raise Puppet::Error.new("File %s does not exist; cannot destroy" % [request.key]) unless File.exist?(path) - - Puppet.notice "Removing file %s %s at '%s'" % [model, request.key, path] begin - File.unlink(path) + removed = false + Dir.glob(File.join(data_directory, request.key.to_s + ".*")).each do |file| + removed = true + File.unlink(file) + end rescue => detail - raise Puppet::Error, "Could not remove %s: %s" % [request.key, detail] + raise Puppet::Error, "Could not remove #{request.key}: #{detail}" end + + raise Puppet::Error, "Could not find files for #{request.key} to remove" unless removed end # Return a model instance for a given file on disk. def find(request) - if respond_to?(:path) - path = path(request.key) - else - path = request.key - end + return nil unless path = latest_path(request) + format = file_format(path) - return nil unless File.exist?(path) + raise ArgumentError, "File format #{format} is not supported by #{self.class.indirection_name}" unless model.support_format?(format) begin - content = File.read(path) + return model.convert_from(format, File.read(path)) rescue => detail - raise Puppet::Error, "Could not retrieve path %s: %s" % [path, detail] + raise Puppet::Error, "Could not convert path #{path} into a #{self.class.indirection_name}: #{detail}" end - - return model.new(content) end # Save a new file to disk. def save(request) - if respond_to?(:path) - path = path(request.key) - else - path = request.key - end + path = file_path(request) + dir = File.dirname(path) - raise Puppet::Error.new("Cannot save %s; parent directory %s does not exist" % [request.key, dir]) unless File.directory?(dir) + raise Puppet::Error.new("Cannot save #{request.key}; parent directory #{dir} does not exist") unless File.directory?(dir) begin - File.open(path, "w") { |f| f.print request.instance.content } + File.open(path, "w") { |f| f.print request.instance.render(serialization_format) } rescue => detail - raise Puppet::Error, "Could not write %s: %s" % [request.key, detail] + raise Puppet::Error, "Could not write #{request.key}: #{detail}" % [request.key, detail] end end + + def path(key) + key + end end diff --git a/spec/unit/indirector/file.rb b/spec/unit/indirector/file.rb index c19d8bee1..4b3532e24 100755 --- a/spec/unit/indirector/file.rb +++ b/spec/unit/indirector/file.rb @@ -1,161 +1,181 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/file' describe Puppet::Indirector::File do before :each do Puppet::Indirector::Terminus.stubs(:register_terminus_class) @model = mock 'model' @indirection = stub 'indirection', :name => :mystuff, :register_terminus_type => nil, :model => @model Puppet::Indirector::Indirection.stubs(:instance).returns(@indirection) @file_class = Class.new(Puppet::Indirector::File) do def self.to_s "Testing::Mytype" end end @searcher = @file_class.new @path = "/my/file" @dir = "/my" @request = stub 'request', :key => @path end describe Puppet::Indirector::File, " when finding files" do - it "should provide a method to return file contents at a specified path" do @searcher.should respond_to(:find) end - it "should return file contents as an instance of the model" do - content = "my content" + it "should use the server data directory plus the indirection name if the process name is 'puppetmasterd'" do + Puppet.settings.expects(:value).with(:name).returns "puppetmasterd" + Puppet.settings.expects(:value).with(:server_datadir).returns "/my/dir" + + @searcher.data_directory.should == File.join("/my/dir", "mystuff") + end - file = mock 'file' - @model.expects(:new).with(content).returns(file) + it "should use the client data directory plus the indirection name if the process name is not 'puppetmasterd'" do + Puppet.settings.expects(:value).with(:name).returns "puppetd" + Puppet.settings.expects(:value).with(:client_datadir).returns "/my/dir" - File.expects(:exist?).with(@path).returns(true) - File.expects(:read).with(@path).returns(content) - @searcher.find(@request) + @searcher.data_directory.should == File.join("/my/dir", "mystuff") end - it "should create the model instance with the content as the only argument to initialization" do - content = "my content" + it "should use the newest file in the data directory matching the indirection key without extension" do + @searcher.expects(:data_directory).returns "/data/dir" + @request.stubs(:key).returns "foo" + Dir.expects(:glob).with("/data/dir/foo.*").returns %w{/data1.stuff /data2.stuff} - file = mock 'file' - @model.expects(:new).with(content).returns(file) + stat1 = stub 'data1', :mtime => (Time.now - 5) + stat2 = stub 'data2', :mtime => Time.now + File.expects(:stat).with("/data1.stuff").returns stat1 + File.expects(:stat).with("/data2.stuff").returns stat2 - File.expects(:exist?).with(@path).returns(true) - File.expects(:read).with(@path).returns(content) - @searcher.find(@request).should equal(file) + @searcher.latest_path(@request).should == "/data2.stuff" end - it "should return nil if no file is found" do - File.expects(:exist?).with(@path).returns(false) + it "should return nil when no files are found" do + @searcher.stubs(:latest_path).returns nil + @searcher.find(@request).should be_nil end - it "should fail intelligently if a found file cannot be read" do - File.expects(:exist?).with(@path).returns(true) - File.expects(:read).with(@path).raises(RuntimeError) - proc { @searcher.find(@request) }.should raise_error(Puppet::Error) + it "should determine the file format from the file extension" do + @searcher.file_format("/data2.pson").should == "pson" end - it "should use the path() method to calculate the path if it exists" do - @searcher.meta_def(:path) do |name| - name.upcase - end + it "should fail if the model does not support the file format" do + @searcher.stubs(:latest_path).returns "/my/file.pson" - File.expects(:exist?).with(@path.upcase).returns(false) - @searcher.find(@request) + @model.expects(:support_format?).with("pson").returns false + + lambda { @searcher.find(@request) }.should raise_error(ArgumentError) end end describe Puppet::Indirector::File, " when saving files" do before do @content = "my content" - @file = stub 'file', :content => @content, :path => @path, :name => @path + @file = stub 'file', :content => @content, :path => @path, :name => @path, :render => "mydata" @request.stubs(:instance).returns @file end it "should provide a method to save file contents at a specified path" do - filehandle = mock 'file' - File.expects(:directory?).with(@dir).returns(true) - File.expects(:open).with(@path, "w").yields(filehandle) - filehandle.expects(:print).with(@content) + @searcher.should respond_to(:save) + end - @searcher.save(@request) + it "should choose the file extension based on the default format of the model" do + @model.expects(:default_format).returns "pson" + + @searcher.serialization_format.should == "pson" end - it "should fail intelligently if the file's parent directory does not exist" do - File.expects(:directory?).with(@dir).returns(false) + it "should place the file in the data directory, named after the indirection, key, and format" do + @searcher.stubs(:data_directory).returns "/my/dir" + @searcher.stubs(:serialization_format).returns "pson" - proc { @searcher.save(@request) }.should raise_error(Puppet::Error) + @request.stubs(:key).returns "foo" + @searcher.file_path(@request).should == File.join("/my/dir", "foo.pson") end - it "should fail intelligently if a file cannot be written" do - filehandle = mock 'file' - File.expects(:directory?).with(@dir).returns(true) - File.expects(:open).with(@path, "w").yields(filehandle) - filehandle.expects(:print).with(@content).raises(ArgumentError) + it "should fail intelligently if the file's parent directory does not exist" do + @searcher.stubs(:file_path).returns "/my/dir/file.pson" + @searcher.stubs(:serialization_format).returns "pson" + + @request.stubs(:key).returns "foo" + File.expects(:directory?).with(File.join("/my/dir")).returns(false) proc { @searcher.save(@request) }.should raise_error(Puppet::Error) end - it "should use the path() method to calculate the path if it exists" do - @searcher.meta_def(:path) do |name| - name.upcase - end + it "should render the instance using the file format and print it to the file path" do + @searcher.stubs(:file_path).returns "/my/file.pson" + @searcher.stubs(:serialization_format).returns "pson" + + File.stubs(:directory?).returns true + + @request.instance.expects(:render).with("pson").returns "data" - # Reset the key to something without a parent dir, so no checks are necessary - @request.stubs(:key).returns "/my" + fh = mock 'filehandle' + File.expects(:open).with("/my/file.pson", "w").yields fh + fh.expects(:print).with("data") - File.expects(:open).with("/MY", "w") @searcher.save(@request) end + + it "should fail intelligently if a file cannot be written" do + filehandle = mock 'file' + File.stubs(:directory?).returns(true) + File.stubs(:open).yields(filehandle) + filehandle.expects(:print).raises(ArgumentError) + + @searcher.stubs(:file_path).returns "/my/file.pson" + @model.stubs(:default_format).returns "pson" + + @instance.stubs(:render).returns "stuff" + + proc { @searcher.save(@request) }.should raise_error(Puppet::Error) + end end describe Puppet::Indirector::File, " when removing files" do + it "should provide a method to remove files" do + @searcher.should respond_to(:destroy) + end - it "should provide a method to remove files at a specified path" do - File.expects(:exist?).with(@path).returns(true) - File.expects(:unlink).with(@path) + it "should remove files in all formats found in the data directory that match the request key" do + @searcher.stubs(:data_directory).returns "/my/dir" + @request.stubs(:key).returns "me" - @searcher.destroy(@request) - end + Dir.expects(:glob).with(File.join("/my/dir", "me.*")).returns %w{/one /two} - it "should throw an exception if the file is not found" do - File.expects(:exist?).with(@path).returns(false) + File.expects(:unlink).with("/one") + File.expects(:unlink).with("/two") - proc { @searcher.destroy(@request) }.should raise_error(Puppet::Error) + @searcher.destroy(@request) end - it "should fail intelligently if the file cannot be removed" do - File.expects(:exist?).with(@path).returns(true) - File.expects(:unlink).with(@path).raises(ArgumentError) + it "should throw an exception if no file is found" do + @searcher.stubs(:data_directory).returns "/my/dir" + @request.stubs(:key).returns "me" + + Dir.expects(:glob).with(File.join("/my/dir", "me.*")).returns [] proc { @searcher.destroy(@request) }.should raise_error(Puppet::Error) end - it "should log that is removing the file" do - File.expects(:exist?).returns(true) - File.expects(:unlink) - Puppet.expects(:notice) - @searcher.destroy(@request) - end + it "should fail intelligently if a file cannot be removed" do + @searcher.stubs(:data_directory).returns "/my/dir" + @request.stubs(:key).returns "me" - it "should use the path() method to calculate the path if it exists" do - @searcher.meta_def(:path) do |thing| - thing.to_s.upcase - end + Dir.expects(:glob).with(File.join("/my/dir", "me.*")).returns %w{/one} - File.expects(:exist?).with("/MY/FILE").returns(true) - File.expects(:unlink).with("/MY/FILE") + File.expects(:unlink).with("/one").raises ArgumentError - @searcher.destroy(@request) + proc { @searcher.destroy(@request) }.should raise_error(Puppet::Error) end end end diff --git a/spec/unit/indirector/yaml.rb b/spec/unit/indirector/yaml.rb index 7536fbc25..a461b0c0c 100755 --- a/spec/unit/indirector/yaml.rb +++ b/spec/unit/indirector/yaml.rb @@ -1,145 +1,145 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/yaml' describe Puppet::Indirector::Yaml, " when choosing file location" do before :each do @indirection = stub 'indirection', :name => :my_yaml, :register_terminus_type => nil Puppet::Indirector::Indirection.stubs(:instance).with(:my_yaml).returns(@indirection) @store_class = Class.new(Puppet::Indirector::Yaml) do def self.to_s "MyYaml::MyType" end end @store = @store_class.new @subject = Object.new @subject.metaclass.send(:attr_accessor, :name) @subject.name = :me @dir = "/what/ever" Puppet.settings.stubs(:value).returns("fakesettingdata") Puppet.settings.stubs(:value).with(:clientyamldir).returns(@dir) @request = stub 'request', :key => :me, :instance => @subject end describe Puppet::Indirector::Yaml, " when choosing file location" do - it "should use the yamldir if the process name is 'puppetmasterd'" do + it "should use the server_datadir if the process name is 'puppetmasterd'" do Puppet.settings.expects(:value).with(:name).returns "puppetmasterd" - Puppet.settings.expects(:value).with(:yamldir).returns "/main/yaml/dir" - @store.path(:me).should =~ %r{^/main/yaml/dir} + Puppet.settings.expects(:value).with(:server_datadir).returns "/server/data/dir" + @store.path(:me).should =~ %r{^/server/data/dir} end it "should use the client yamldir if the process name is not 'puppetmasterd'" do Puppet.settings.expects(:value).with(:name).returns "cient" Puppet.settings.expects(:value).with(:clientyamldir).returns "/client/yaml/dir" @store.path(:me).should =~ %r{^/client/yaml/dir} end it "should store all files in a single file root set in the Puppet defaults" do @store.path(:me).should =~ %r{^#{@dir}} end it "should use the terminus name for choosing the subdirectory" do @store.path(:me).should =~ %r{^#{@dir}/my_yaml} end it "should use the object's name to determine the file name" do @store.path(:me).should =~ %r{me.yaml$} end end describe Puppet::Indirector::Yaml, " when storing objects as YAML" do it "should only store objects that respond to :name" do @request.stubs(:instance).returns Object.new proc { @store.save(@request) }.should raise_error(ArgumentError) end it "should convert Ruby objects to YAML and write them to disk using a write lock" do yaml = @subject.to_yaml file = mock 'file' path = @store.send(:path, @subject.name) FileTest.expects(:exist?).with(File.dirname(path)).returns(true) @store.expects(:writelock).with(path, 0660).yields(file) file.expects(:print).with(yaml) @store.save(@request) end it "should create the indirection subdirectory if it does not exist" do yaml = @subject.to_yaml file = mock 'file' path = @store.send(:path, @subject.name) dir = File.dirname(path) FileTest.expects(:exist?).with(dir).returns(false) Dir.expects(:mkdir).with(dir) @store.expects(:writelock).yields(file) file.expects(:print).with(yaml) @store.save(@request) end end describe Puppet::Indirector::Yaml, " when retrieving YAML" do it "should read YAML in from disk using a read lock and convert it to Ruby objects" do path = @store.send(:path, @subject.name) yaml = @subject.to_yaml FileTest.expects(:exist?).with(path).returns(true) fh = mock 'filehandle' @store.expects(:readlock).with(path).yields fh fh.expects(:read).returns yaml @store.find(@request).instance_variable_get("@name").should == :me end it "should fail coherently when the stored YAML is invalid" do path = @store.send(:path, @subject.name) FileTest.expects(:exist?).with(path).returns(true) # Something that will fail in yaml yaml = "--- !ruby/object:Hash" fh = mock 'filehandle' @store.expects(:readlock).yields fh fh.expects(:read).returns yaml proc { @store.find(@request) }.should raise_error(Puppet::Error) end end describe Puppet::Indirector::Yaml, " when searching" do it "should return an array of fact instances with one instance for each file when globbing *" do @request = stub 'request', :key => "*", :instance => @subject @one = mock 'one' @two = mock 'two' @store.expects(:base).returns "/my/yaml/dir" Dir.expects(:glob).with(File.join("/my/yaml/dir", @store.class.indirection_name.to_s, @request.key)).returns(%w{one.yaml two.yaml}) YAML.expects(:load_file).with("one.yaml").returns @one; YAML.expects(:load_file).with("two.yaml").returns @two; @store.search(@request).should == [@one, @two] end it "should return an array containing a single instance of fact when globbing 'one*'" do @request = stub 'request', :key => "one*", :instance => @subject @one = mock 'one' @store.expects(:base).returns "/my/yaml/dir" Dir.expects(:glob).with(File.join("/my/yaml/dir", @store.class.indirection_name.to_s, @request.key)).returns(%w{one.yaml}) YAML.expects(:load_file).with("one.yaml").returns @one; @store.search(@request).should == [@one] end it "should return an empty array when the glob doesn't match anything" do @request = stub 'request', :key => "f*ilglobcanfail*", :instance => @subject @store.expects(:base).returns "/my/yaml/dir" Dir.expects(:glob).with(File.join("/my/yaml/dir", @store.class.indirection_name.to_s, @request.key)).returns([]) @store.search(@request).should == [] end end end