diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index e3203c81e..846c8bcc5 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -1,509 +1,509 @@ # # Copyright 2011 Bryan Kearney # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require 'augeas' if Puppet.features.augeas? require 'strscan' require 'puppet/util' require 'puppet/util/diff' require 'puppet/util/package' Puppet::Type.type(:augeas).provide(:augeas) do include Puppet::Util include Puppet::Util::Diff include Puppet::Util::Package confine :feature => :augeas has_features :parse_commands, :need_to_run?,:execute_changes SAVE_NOOP = "noop" SAVE_OVERWRITE = "overwrite" SAVE_NEWFILE = "newfile" SAVE_BACKUP = "backup" COMMANDS = { "set" => [ :path, :string ], "setm" => [ :path, :string, :string ], "rm" => [ :path ], "clear" => [ :path ], "clearm" => [ :path, :string ], "mv" => [ :path, :path ], "insert" => [ :string, :string, :path ], "get" => [ :path, :comparator, :string ], "defvar" => [ :string, :path ], "defnode" => [ :string, :path, :string ], "match" => [ :path, :glob ], "size" => [:comparator, :int], "include" => [:string], "not_include" => [:string], "==" => [:glob], "!=" => [:glob] } COMMANDS["ins"] = COMMANDS["insert"] COMMANDS["remove"] = COMMANDS["rm"] COMMANDS["move"] = COMMANDS["mv"] attr_accessor :aug # Extracts an 2 dimensional array of commands which are in the # form of command path value. # The input can be # - A string with one command # - A string with many commands per line # - An array of strings. def parse_commands(data) context = resource[:context] # Add a trailing / if it is not there if (context.length > 0) context << "/" if context[-1, 1] != "/" end data = data.split($/) if data.is_a?(String) data = data.flatten args = [] data.each do |line| line.strip! next if line.nil? || line.empty? argline = [] sc = StringScanner.new(line) cmd = sc.scan(/\w+|==|!=/) formals = COMMANDS[cmd] fail("Unknown command #{cmd}") unless formals argline << cmd narg = 0 formals.each do |f| sc.skip(/\s+/) narg += 1 if f == :path start = sc.pos nbracket = 0 inSingleTick = false inDoubleTick = false begin sc.skip(/([^\]\[\s\\'"]|\\.)+/) ch = sc.getch nbracket += 1 if ch == "[" nbracket -= 1 if ch == "]" inSingleTick = !inSingleTick if ch == "'" inDoubleTick = !inDoubleTick if ch == "\"" fail("unmatched [") if nbracket < 0 end until ((nbracket == 0 && !inSingleTick && !inDoubleTick && (ch =~ /\s/)) || sc.eos?) len = sc.pos - start len -= 1 unless sc.eos? unless p = sc.string[start, len] fail("missing path argument #{narg} for #{cmd}") end # Rip off any ticks if they are there. p = p[1, (p.size - 2)] if p[0,1] == "'" || p[0,1] == "\"" p.chomp!("/") if p[0,1] != '$' && p[0,1] != "/" argline << context + p else argline << p end elsif f == :string delim = sc.peek(1) if delim == "'" || delim == "\"" sc.getch argline << sc.scan(/([^\\#{delim}]|(\\.))*/) sc.getch else argline << sc.scan(/[^\s]+/) end fail("missing string argument #{narg} for #{cmd}") unless argline[-1] elsif f == :comparator argline << sc.scan(/(==|!=|=~|<=|>=|<|>)/) unless argline[-1] puts sc.rest fail("invalid comparator for command #{cmd}") end elsif f == :int argline << sc.scan(/\d+/).to_i elsif f== :glob argline << sc.rest end end args << argline end args end def open_augeas unless @aug flags = Augeas::NONE flags = Augeas::TYPE_CHECK if resource[:type_check] == :true if resource[:incl] flags |= Augeas::NO_MODL_AUTOLOAD else flags |= Augeas::NO_LOAD end root = resource[:root] load_path = get_load_path(resource) debug("Opening augeas with root #{root}, lens path #{load_path}, flags #{flags}") @aug = Augeas::open(root, load_path,flags) debug("Augeas version #{get_augeas_version} is installed") if versioncmp(get_augeas_version, "0.3.6") >= 0 # Optimize loading if the context is given and it's a simple path, # requires the glob function from Augeas 0.8.2 or up glob_avail = !aug.match("/augeas/version/pathx/functions/glob").empty? opt_ctx = resource[:context].match("^/files/[^'\"\\[\\]]+$") if resource[:context] restricted = false if resource[:incl] aug.set("/augeas/load/Xfm/lens", resource[:lens]) aug.set("/augeas/load/Xfm/incl", resource[:incl]) restricted_metadata = "/augeas//error" elsif glob_avail and opt_ctx # Optimize loading if the context is given, requires the glob function # from Augeas 0.8.2 or up ctx_path = resource[:context].sub(/^\/files(.*?)\/?$/, '\1/') load_path = "/augeas/load/*['%s' !~ glob(incl) + regexp('/.*')]" % ctx_path if aug.match(load_path).size < aug.match("/augeas/load/*").size aug.rm(load_path) restricted_metadata = "/augeas/files#{ctx_path}/error" else # This will occur if the context is less specific than any glob debug("Unable to optimize files loaded by context path, no glob matches") end end aug.load print_load_errors(restricted_metadata) end @aug end def close_augeas if @aug @aug.close debug("Closed the augeas connection") @aug = nil end end def is_numeric?(s) case s when Fixnum true when String s.match(/\A[+-]?\d+?(\.\d+)?\Z/n) == nil ? false : true else false end end # Used by the need_to_run? method to process get filters. Returns # true if there is a match, false if otherwise # Assumes a syntax of get /files/path [COMPARATOR] value def process_get(cmd_array) return_value = false #validate and tear apart the command fail ("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 4 cmd = cmd_array.shift path = cmd_array.shift comparator = cmd_array.shift arg = cmd_array.join(" ") #check the value in augeas result = @aug.get(path) || '' if ['<', '<=', '>=', '>'].include? comparator and is_numeric?(result) and is_numeric?(arg) resultf = result.to_f argf = arg.to_f return_value = (resultf.send(comparator, argf)) elsif comparator == "!=" return_value = (result != arg) elsif comparator == "=~" regex = Regexp.new(arg) return_value = (result =~ regex) else return_value = (result.send(comparator, arg)) end !!return_value end # Used by the need_to_run? method to process match filters. Returns # true if there is a match, false if otherwise def process_match(cmd_array) return_value = false #validate and tear apart the command fail("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 3 cmd = cmd_array.shift path = cmd_array.shift # Need to break apart the clause clause_array = parse_commands(cmd_array.shift)[0] verb = clause_array.shift #Get the values from augeas result = @aug.match(path) || [] fail("Error trying to match path '#{path}'") if (result == -1) # Now do the work case verb when "size" fail("Invalid command: #{cmd_array.join(" ")}") if clause_array.length != 2 comparator = clause_array.shift arg = clause_array.shift case comparator when "!=" return_value = !(result.size.send(:==, arg)) else return_value = (result.size.send(comparator, arg)) end when "include" arg = clause_array.shift return_value = result.include?(arg) when "not_include" arg = clause_array.shift return_value = !result.include?(arg) when "==" begin arg = clause_array.shift new_array = eval arg return_value = (result == new_array) rescue fail("Invalid array in command: #{cmd_array.join(" ")}") end when "!=" begin arg = clause_array.shift new_array = eval arg return_value = (result != new_array) rescue fail("Invalid array in command: #{cmd_array.join(" ")}") end end !!return_value end # Generate lens load paths from user given paths and local pluginsync dir def get_load_path(resource) load_path = [] # Permits colon separated strings or arrays if resource[:load_path] load_path = [resource[:load_path]].flatten load_path.map! { |path| path.split(/:/) } load_path.flatten! end if Puppet::FileSystem.exist?("#{Puppet[:libdir]}/augeas/lenses") load_path << "#{Puppet[:libdir]}/augeas/lenses" end load_path.join(":") end def get_augeas_version @aug.get("/augeas/version") || "" end def set_augeas_save_mode(mode) @aug.set("/augeas/save", mode) end def print_load_errors(path) errors = @aug.match("/augeas//error") unless errors.empty? if path && !@aug.match(path).empty? warning("Loading failed for one or more files, see debug for /augeas//error output") else debug("Loading failed for one or more files, output from /augeas//error:") end end print_errors(errors) end def print_put_errors errors = @aug.match("/augeas//error[. = 'put_failed']") debug("Put failed on one or more files, output from /augeas//error:") unless errors.empty? print_errors(errors) end def print_errors(errors) errors.each do |errnode| error = @aug.get(errnode) debug("#{errnode} = #{error}") unless error.nil? @aug.match("#{errnode}/*").each do |subnode| subvalue = @aug.get(subnode) debug("#{subnode} = #{subvalue}") end end end # Determines if augeas actually needs to run. def need_to_run? force = resource[:force] return_value = true begin open_augeas filter = resource[:onlyif] unless filter == "" cmd_array = parse_commands(filter)[0] command = cmd_array[0]; begin case command when "get"; return_value = process_get(cmd_array) when "match"; return_value = process_match(cmd_array) end rescue SystemExit,NoMemoryError raise rescue Exception => e fail("Error sending command '#{command}' with params #{cmd_array[1..-1].inspect}/#{e.message}") end end unless force # If we have a verison of augeas which is at least 0.3.6 then we # can make the changes now and see if changes were made. if return_value and versioncmp(get_augeas_version, "0.3.6") >= 0 debug("Will attempt to save and only run if files changed") # Execute in NEWFILE mode so we can show a diff set_augeas_save_mode(SAVE_NEWFILE) do_execute_changes save_result = @aug.save unless save_result print_put_errors fail("Saving failed, see debug") end saved_files = @aug.match("/augeas/events/saved") if saved_files.size > 0 root = resource[:root].sub(/^\/$/, "") saved_files.map! {|key| @aug.get(key).sub(/^\/files/, root) } saved_files.uniq.each do |saved_file| if Puppet[:show_diff] - notice "\n" + diff(saved_file, saved_file + ".augnew") + self.send(@resource[:loglevel], "\n" + diff(saved_file, saved_file + ".augnew")) end File.delete(saved_file + ".augnew") end debug("Files changed, should execute") return_value = true else debug("Skipping because no files were changed") return_value = false end end end ensure if not return_value or resource.noop? or not save_result close_augeas end end return_value end def execute_changes # Workaround Augeas bug where changing the save mode doesn't trigger a # reload of the previously saved file(s) when we call Augeas#load @aug.match("/augeas/events/saved").each do |file| @aug.rm("/augeas#{@aug.get(file)}/mtime") end # Reload augeas, and execute the changes for real set_augeas_save_mode(SAVE_OVERWRITE) if versioncmp(get_augeas_version, "0.3.6") >= 0 @aug.load do_execute_changes unless @aug.save print_put_errors fail("Save failed, see debug") end :executed ensure close_augeas end # Actually execute the augeas changes. def do_execute_changes commands = parse_commands(resource[:changes]) commands.each do |cmd_array| fail("invalid command #{cmd_array.join[" "]}") if cmd_array.length < 2 command = cmd_array[0] cmd_array.shift begin case command when "set" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.set(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "setm" if aug.respond_to?(command) debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.setm(cmd_array[0], cmd_array[1], cmd_array[2]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) else fail("command '#{command}' not supported in installed version of ruby-augeas") end when "rm", "remove" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.rm(cmd_array[0]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) when "clear" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.clear(cmd_array[0]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "clearm" # Check command exists ... doesn't currently in ruby-augeas 0.4.1 if aug.respond_to?(command) debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.clearm(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) else fail("command '#{command}' not supported in installed version of ruby-augeas") end when "insert", "ins" label = cmd_array[0] where = cmd_array[1] path = cmd_array[2] case where when "before"; before = true when "after"; before = false else fail("Invalid value '#{where}' for where param") end debug("sending command '#{command}' with params #{[label, where, path].inspect}") rv = aug.insert(path, label, before) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) when "defvar" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.defvar(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "defnode" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.defnode(cmd_array[0], cmd_array[1], cmd_array[2]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "mv", "move" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.mv(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) else fail("Command '#{command}' is not supported") end rescue SystemExit,NoMemoryError raise rescue Exception => e fail("Error sending command '#{command}' with params #{cmd_array.inspect}/#{e.message}") end end end end diff --git a/spec/unit/provider/augeas/augeas_spec.rb b/spec/unit/provider/augeas/augeas_spec.rb index b79a13dd3..b67eb5c2b 100755 --- a/spec/unit/provider/augeas/augeas_spec.rb +++ b/spec/unit/provider/augeas/augeas_spec.rb @@ -1,903 +1,904 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/util/package' provider_class = Puppet::Type.type(:augeas).provider(:augeas) describe provider_class do before(:each) do @resource = Puppet::Type.type(:augeas).new( :name => "test", :root => my_fixture_dir, :provider => :augeas ) @provider = provider_class.new(@resource) end after(:each) do @provider.close_augeas end describe "command parsing" do it "should break apart a single line into three tokens and clean up the context" do @resource[:context] = "/context" tokens = @provider.parse_commands("set Jar/Jar Binks") tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == "/context/Jar/Jar" tokens[0][2].should == "Binks" end it "should break apart a multiple line into six tokens" do tokens = @provider.parse_commands("set /Jar/Jar Binks\nrm anakin") tokens.size.should == 2 tokens[0].size.should == 3 tokens[1].size.should == 2 tokens[0][0].should == "set" tokens[0][1].should == "/Jar/Jar" tokens[0][2].should == "Binks" tokens[1][0].should == "rm" tokens[1][1].should == "anakin" end it "should strip whitespace and ignore blank lines" do tokens = @provider.parse_commands(" set /Jar/Jar Binks \t\n \n\n rm anakin ") tokens.size.should == 2 tokens[0].size.should == 3 tokens[1].size.should == 2 tokens[0][0].should == "set" tokens[0][1].should == "/Jar/Jar" tokens[0][2].should == "Binks" tokens[1][0].should == "rm" tokens[1][1].should == "anakin" end it "should handle arrays" do @resource[:context] = "/foo/" commands = ["set /Jar/Jar Binks", "rm anakin"] tokens = @provider.parse_commands(commands) tokens.size.should == 2 tokens[0].size.should == 3 tokens[1].size.should == 2 tokens[0][0].should == "set" tokens[0][1].should == "/Jar/Jar" tokens[0][2].should == "Binks" tokens[1][0].should == "rm" tokens[1][1].should == "/foo/anakin" end # This is not supported in the new parsing class #it "should concat the last values" do # provider = provider_class.new # tokens = provider.parse_commands("set /Jar/Jar Binks is my copilot") # tokens.size.should == 1 # tokens[0].size.should == 3 # tokens[0][0].should == "set" # tokens[0][1].should == "/Jar/Jar" # tokens[0][2].should == "Binks is my copilot" #end it "should accept spaces in the value and single ticks" do @resource[:context] = "/foo/" tokens = @provider.parse_commands("set JarJar 'Binks is my copilot'") tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == "/foo/JarJar" tokens[0][2].should == "Binks is my copilot" end it "should accept spaces in the value and double ticks" do @resource[:context] = "/foo/" tokens = @provider.parse_commands('set /JarJar "Binks is my copilot"') tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == '/JarJar' tokens[0][2].should == 'Binks is my copilot' end it "should accept mixed ticks" do @resource[:context] = "/foo/" tokens = @provider.parse_commands('set JarJar "Some \'Test\'"') tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == '/foo/JarJar' tokens[0][2].should == "Some \'Test\'" end it "should handle predicates with literals" do @resource[:context] = "/foo/" tokens = @provider.parse_commands("rm */*[module='pam_console.so']") tokens.should == [["rm", "/foo/*/*[module='pam_console.so']"]] end it "should handle whitespace in predicates" do @resource[:context] = "/foo/" tokens = @provider.parse_commands("ins 42 before /files/etc/hosts/*/ipaddr[ . = '127.0.0.1' ]") tokens.should == [["ins", "42", "before","/files/etc/hosts/*/ipaddr[ . = '127.0.0.1' ]"]] end it "should handle multiple predicates" do @resource[:context] = "/foo/" tokens = @provider.parse_commands("clear pam.d/*/*[module = 'system-auth'][type = 'account']") tokens.should == [["clear", "/foo/pam.d/*/*[module = 'system-auth'][type = 'account']"]] end it "should handle nested predicates" do @resource[:context] = "/foo/" args = ["clear", "/foo/pam.d/*/*[module[ ../type = 'type] = 'system-auth'][type[last()] = 'account']"] tokens = @provider.parse_commands(args.join(" ")) tokens.should == [ args ] end it "should handle escaped doublequotes in doublequoted string" do @resource[:context] = "/foo/" tokens = @provider.parse_commands("set /foo \"''\\\"''\"") tokens.should == [[ "set", "/foo", "''\\\"''" ]] end it "should allow escaped spaces and brackets in paths" do @resource[:context] = "/foo/" args = [ "set", "/white\\ space/\\[section", "value" ] tokens = @provider.parse_commands(args.join(" \t ")) tokens.should == [ args ] end it "should allow single quoted escaped spaces in paths" do @resource[:context] = "/foo/" args = [ "set", "'/white\\ space/key'", "value" ] tokens = @provider.parse_commands(args.join(" \t ")) tokens.should == [[ "set", "/white\\ space/key", "value" ]] end it "should allow double quoted escaped spaces in paths" do @resource[:context] = "/foo/" args = [ "set", '"/white\\ space/key"', "value" ] tokens = @provider.parse_commands(args.join(" \t ")) tokens.should == [[ "set", "/white\\ space/key", "value" ]] end it "should remove trailing slashes" do @resource[:context] = "/foo/" tokens = @provider.parse_commands("set foo/ bar") tokens.should == [[ "set", "/foo/foo", "bar" ]] end end describe "get filters" do before do augeas = stub("augeas", :get => "value") augeas.stubs("close") @provider.aug = augeas end it "should return false for a = nonmatch" do command = ["get", "fake value", "==", "value"] @provider.process_get(command).should == true end it "should return true for a != match" do command = ["get", "fake value", "!=", "value"] @provider.process_get(command).should == false end it "should return true for a =~ match" do command = ["get", "fake value", "=~", "val*"] @provider.process_get(command).should == true end it "should return false for a == nonmatch" do command = ["get", "fake value", "=~", "num*"] @provider.process_get(command).should == false end end describe "match filters" do before do augeas = stub("augeas", :match => ["set", "of", "values"]) augeas.stubs("close") @provider = provider_class.new(@resource) @provider.aug = augeas end it "should return true for size match" do command = ["match", "fake value", "size == 3"] @provider.process_match(command).should == true end it "should return false for a size non match" do command = ["match", "fake value", "size < 3"] @provider.process_match(command).should == false end it "should return true for includes match" do command = ["match", "fake value", "include values"] @provider.process_match(command).should == true end it "should return false for includes non match" do command = ["match", "fake value", "include JarJar"] @provider.process_match(command).should == false end it "should return true for includes match" do command = ["match", "fake value", "not_include JarJar"] @provider.process_match(command).should == true end it "should return false for includes non match" do command = ["match", "fake value", "not_include values"] @provider.process_match(command).should == false end it "should return true for an array match" do command = ["match", "fake value", "== ['set', 'of', 'values']"] @provider.process_match(command).should == true end it "should return false for an array non match" do command = ["match", "fake value", "== ['this', 'should', 'not', 'match']"] @provider.process_match(command).should == false end it "should return false for an array match with noteq" do command = ["match", "fake value", "!= ['set', 'of', 'values']"] @provider.process_match(command).should == false end it "should return true for an array non match with noteq" do command = ["match", "fake value", "!= ['this', 'should', 'not', 'match']"] @provider.process_match(command).should == true end end describe "need to run" do before(:each) do @augeas = stub("augeas") @augeas.stubs("close") @provider.aug = @augeas # These tests pretend to be an earlier version so the provider doesn't # attempt to make the change in the need_to_run? method @provider.stubs(:get_augeas_version).returns("0.3.5") end it "should handle no filters" do @augeas.stubs("match").returns(["set", "of", "values"]) @provider.need_to_run?.should == true end it "should return true when a get filter matches" do @resource[:onlyif] = "get path == value" @augeas.stubs("get").returns("value") @provider.need_to_run?.should == true end describe "performing numeric comparisons (#22617)" do it "should return true when a get string compare is true" do @resource[:onlyif] = "get bpath > a" @augeas.stubs("get").returns("b") @provider.need_to_run?.should == true end it "should return false when a get string compare is false" do @resource[:onlyif] = "get a19path > a2" @augeas.stubs("get").returns("a19") @provider.need_to_run?.should == false end it "should return true when a get int gt compare is true" do @resource[:onlyif] = "get path19 > 2" @augeas.stubs("get").returns("19") @provider.need_to_run?.should == true end it "should return true when a get int ge compare is true" do @resource[:onlyif] = "get path19 >= 2" @augeas.stubs("get").returns("19") @provider.need_to_run?.should == true end it "should return true when a get int lt compare is true" do @resource[:onlyif] = "get path2 < 19" @augeas.stubs("get").returns("2") @provider.need_to_run?.should == true end it "should return false when a get int le compare is false" do @resource[:onlyif] = "get path39 <= 4" @augeas.stubs("get").returns("39") @provider.need_to_run?.should == false end end describe "performing is_numeric checks (#22617)" do it "should return false for nil" do @provider.is_numeric?(nil).should == false end it "should return true for Fixnums" do @provider.is_numeric?(9).should == true end it "should return true for numbers in Strings" do @provider.is_numeric?('9').should == true end it "should return false for non-number Strings" do @provider.is_numeric?('x9').should == false end it "should return false for other types" do @provider.is_numeric?([true]).should == false end end it "should return false when a get filter does not match" do @resource[:onlyif] = "get path == another value" @augeas.stubs("get").returns("value") @provider.need_to_run?.should == false end it "should return true when a match filter matches" do @resource[:onlyif] = "match path size == 3" @augeas.stubs("match").returns(["set", "of", "values"]) @provider.need_to_run?.should == true end it "should return false when a match filter does not match" do @resource[:onlyif] = "match path size == 2" @augeas.stubs("match").returns(["set", "of", "values"]) @provider.need_to_run?.should == false end # Now setting force to true it "setting force should not change the above logic" do @resource[:force] = true @resource[:onlyif] = "match path size == 2" @augeas.stubs("match").returns(["set", "of", "values"]) @provider.need_to_run?.should == false end #Ticket 5211 testing it "should return true when a size != the provided value" do @resource[:onlyif] = "match path size != 17" @augeas.stubs("match").returns(["set", "of", "values"]) @provider.need_to_run?.should == true end #Ticket 5211 testing it "should return false when a size doeas equal the provided value" do @resource[:onlyif] = "match path size != 3" @augeas.stubs("match").returns(["set", "of", "values"]) @provider.need_to_run?.should == false end # Ticket 2728 (diff files) describe "and Puppet[:show_diff] is set" do before(:each) do Puppet[:show_diff] = true @resource[:root] = "" @provider.stubs(:get_augeas_version).returns("0.10.0") @augeas.stubs(:set).returns(true) @augeas.stubs(:save).returns(true) end it "should display a diff when a single file is shown to have been changed" do file = "/etc/hosts" File.stubs(:delete) + @resource[:loglevel] = "crit" @resource[:context] = "/files" @resource[:changes] = ["set #{file}/foo bar"] @augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved"]) @augeas.stubs(:get).with("/augeas/events/saved").returns("/files#{file}") @augeas.expects(:set).with("/augeas/save", "newfile") @provider.expects("diff").with("#{file}", "#{file}.augnew").returns("diff") @provider.should be_need_to_run expect(@logs[0].message).to eq("\ndiff") - expect(@logs[0].level).to eq(:notice) + expect(@logs[0].level).to eq(:crit) end it "should display a diff for each file that is changed when changing many files" do file1 = "/etc/hosts" file2 = "/etc/resolv.conf" File.stubs(:delete) @resource[:context] = "/files" @resource[:changes] = ["set #{file1}/foo bar", "set #{file2}/baz biz"] @augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved[1]", "/augeas/events/saved[2]"]) @augeas.stubs(:get).with("/augeas/events/saved[1]").returns("/files#{file1}") @augeas.stubs(:get).with("/augeas/events/saved[2]").returns("/files#{file2}") @augeas.expects(:set).with("/augeas/save", "newfile") @provider.expects(:diff).with("#{file1}", "#{file1}.augnew").returns("diff #{file1}") @provider.expects(:diff).with("#{file2}", "#{file2}.augnew").returns("diff #{file2}") @provider.should be_need_to_run expect(@logs.collect(&:message)).to include("\ndiff #{file1}", "\ndiff #{file2}") expect(@logs.collect(&:level)).to eq([:notice, :notice]) end describe "and resource[:root] is set" do it "should call diff when a file is shown to have been changed" do root = "/tmp/foo" file = "/etc/hosts" File.stubs(:delete) @resource[:context] = "/files" @resource[:changes] = ["set #{file}/foo bar"] @resource[:root] = root @augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved"]) @augeas.stubs(:get).with("/augeas/events/saved").returns("/files#{file}") @augeas.expects(:set).with("/augeas/save", "newfile") @provider.expects(:diff).with("#{root}#{file}", "#{root}#{file}.augnew").returns("diff") @provider.should be_need_to_run expect(@logs[0].message).to eq("\ndiff") expect(@logs[0].level).to eq(:notice) end end it "should not call diff if no files change" do file = "/etc/hosts" @resource[:context] = "/files" @resource[:changes] = ["set #{file}/foo bar"] @augeas.stubs(:match).with("/augeas/events/saved").returns([]) @augeas.expects(:set).with("/augeas/save", "newfile") @augeas.expects(:get).with("/augeas/events/saved").never() @augeas.expects(:close) @provider.expects(:diff).never() @provider.should_not be_need_to_run end it "should cleanup the .augnew file" do file = "/etc/hosts" @resource[:context] = "/files" @resource[:changes] = ["set #{file}/foo bar"] @augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved"]) @augeas.stubs(:get).with("/augeas/events/saved").returns("/files#{file}") @augeas.expects(:set).with("/augeas/save", "newfile") @augeas.expects(:close) File.expects(:delete).with(file + ".augnew") @provider.expects(:diff).with("#{file}", "#{file}.augnew").returns("") @provider.should be_need_to_run end # Workaround for Augeas bug #264 which reports filenames twice it "should handle duplicate /augeas/events/saved filenames" do file = "/etc/hosts" @resource[:context] = "/files" @resource[:changes] = ["set #{file}/foo bar"] @augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved[1]", "/augeas/events/saved[2]"]) @augeas.stubs(:get).with("/augeas/events/saved[1]").returns("/files#{file}") @augeas.stubs(:get).with("/augeas/events/saved[2]").returns("/files#{file}") @augeas.expects(:set).with("/augeas/save", "newfile") @augeas.expects(:close) File.expects(:delete).with(file + ".augnew").once() @provider.expects(:diff).with("#{file}", "#{file}.augnew").returns("").once() @provider.should be_need_to_run end it "should fail with an error if saving fails" do file = "/etc/hosts" @resource[:context] = "/files" @resource[:changes] = ["set #{file}/foo bar"] @augeas.stubs(:save).returns(false) @augeas.stubs(:match).with("/augeas/events/saved").returns([]) @augeas.expects(:close) @provider.expects(:diff).never() @provider.expects(:print_put_errors) lambda { @provider.need_to_run? }.should raise_error(Puppet::Error) end end end describe "augeas execution integration" do before do @augeas = stub("augeas", :load) @augeas.stubs("close") @augeas.stubs(:match).with("/augeas/events/saved").returns([]) @provider.aug = @augeas @provider.stubs(:get_augeas_version).returns("0.3.5") end it "should handle set commands" do @resource[:changes] = "set JarJar Binks" @resource[:context] = "/some/path/" @augeas.expects(:set).with("/some/path/JarJar", "Binks").returns(true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle rm commands" do @resource[:changes] = "rm /Jar/Jar" @augeas.expects(:rm).with("/Jar/Jar") @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle remove commands" do @resource[:changes] = "remove /Jar/Jar" @augeas.expects(:rm).with("/Jar/Jar") @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle clear commands" do @resource[:changes] = "clear Jar/Jar" @resource[:context] = "/foo/" @augeas.expects(:clear).with("/foo/Jar/Jar").returns(true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle ins commands with before" do @resource[:changes] = "ins Binks before Jar/Jar" @resource[:context] = "/foo" @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle ins commands with after" do @resource[:changes] = "ins Binks after /Jar/Jar" @resource[:context] = "/foo" @augeas.expects(:insert).with("/Jar/Jar", "Binks", false) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle ins with no context" do @resource[:changes] = "ins Binks after /Jar/Jar" @augeas.expects(:insert).with("/Jar/Jar", "Binks", false) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle multiple commands" do @resource[:changes] = ["ins Binks after /Jar/Jar", "clear Jar/Jar"] @resource[:context] = "/foo/" @augeas.expects(:insert).with("/Jar/Jar", "Binks", false) @augeas.expects(:clear).with("/foo/Jar/Jar").returns(true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle defvar commands" do @resource[:changes] = "defvar myjar Jar/Jar" @resource[:context] = "/foo/" @augeas.expects(:defvar).with("myjar", "/foo/Jar/Jar").returns(true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should pass through augeas variables without context" do @resource[:changes] = ["defvar myjar Jar/Jar","set $myjar/Binks 1"] @resource[:context] = "/foo/" @augeas.expects(:defvar).with("myjar", "/foo/Jar/Jar").returns(true) # this is the important bit, shouldn't be /foo/$myjar/Binks @augeas.expects(:set).with("$myjar/Binks", "1").returns(true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle defnode commands" do @resource[:changes] = "defnode newjar Jar/Jar[last()+1] Binks" @resource[:context] = "/foo/" @augeas.expects(:defnode).with("newjar", "/foo/Jar/Jar[last()+1]", "Binks").returns(true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle mv commands" do @resource[:changes] = "mv Jar/Jar Binks" @resource[:context] = "/foo/" @augeas.expects(:mv).with("/foo/Jar/Jar", "/foo/Binks").returns(true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle setm commands" do @resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","setm test Jar/Jar Binks"] @resource[:context] = "/foo/" @augeas.expects(:respond_to?).with("setm").returns(true) @augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true) @augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true) @augeas.expects(:setm).with("/foo/test", "Jar/Jar", "Binks").returns(true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should throw error if setm command not supported" do @resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","setm test Jar/Jar Binks"] @resource[:context] = "/foo/" @augeas.expects(:respond_to?).with("setm").returns(false) @augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true) @augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true) expect { @provider.execute_changes }.to raise_error RuntimeError, /command 'setm' not supported/ end it "should handle clearm commands" do @resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","clearm test Jar/Jar"] @resource[:context] = "/foo/" @augeas.expects(:respond_to?).with("clearm").returns(true) @augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true) @augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true) @augeas.expects(:clearm).with("/foo/test", "Jar/Jar").returns(true) @augeas.expects(:save).returns(true) @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should throw error if clearm command not supported" do @resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","clearm test Jar/Jar"] @resource[:context] = "/foo/" @augeas.expects(:respond_to?).with("clearm").returns(false) @augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true) @augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true) expect { @provider.execute_changes }.to raise_error(RuntimeError, /command 'clearm' not supported/) end it "should throw error if saving failed" do @resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","clearm test Jar/Jar"] @resource[:context] = "/foo/" @augeas.expects(:respond_to?).with("clearm").returns(true) @augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true) @augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true) @augeas.expects(:clearm).with("/foo/test", "Jar/Jar").returns(true) @augeas.expects(:save).returns(false) @provider.expects(:print_put_errors) @augeas.expects(:match).returns([]) expect { @provider.execute_changes }.to raise_error(Puppet::Error) end end describe "when making changes", :if => Puppet.features.augeas? do include PuppetSpec::Files it "should not clobber the file if it's a symlink" do Puppet::Util::Storage.stubs(:store) link = tmpfile('link') target = tmpfile('target') FileUtils.touch(target) Puppet::FileSystem.symlink(target, link) resource = Puppet::Type.type(:augeas).new( :name => 'test', :incl => link, :lens => 'Sshd.lns', :changes => "set PermitRootLogin no" ) catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.apply File.ftype(link).should == 'link' Puppet::FileSystem.readlink(link).should == target File.read(target).should =~ /PermitRootLogin no/ end end describe "load/save failure reporting" do before do @augeas = stub("augeas") @augeas.stubs("close") @provider.aug = @augeas end describe "should find load errors" do before do @augeas.expects(:match).with("/augeas//error").returns(["/augeas/files/foo/error"]) @augeas.expects(:match).with("/augeas/files/foo/error/*").returns(["/augeas/files/foo/error/path", "/augeas/files/foo/error/message"]) @augeas.expects(:get).with("/augeas/files/foo/error").returns("some_failure") @augeas.expects(:get).with("/augeas/files/foo/error/path").returns("/foo") @augeas.expects(:get).with("/augeas/files/foo/error/message").returns("Failed to...") end it "and output only to debug when no path supplied" do @provider.expects(:debug).times(5) @provider.expects(:warning).never() @provider.print_load_errors(nil) end it "and output a warning and to debug when path supplied" do @augeas.expects(:match).with("/augeas/files/foo//error").returns(["/augeas/files/foo/error"]) @provider.expects(:warning).once() @provider.expects(:debug).times(4) @provider.print_load_errors('/augeas/files/foo//error') end it "and output only to debug when path doesn't match" do @augeas.expects(:match).with("/augeas/files/foo//error").returns([]) @provider.expects(:warning).never() @provider.expects(:debug).times(5) @provider.print_load_errors('/augeas/files/foo//error') end end it "should find load errors from lenses" do @augeas.expects(:match).with("/augeas//error").twice.returns(["/augeas/load/Xfm/error"]) @augeas.expects(:match).with("/augeas/load/Xfm/error/*").returns([]) @augeas.expects(:get).with("/augeas/load/Xfm/error").returns(["Could not find lens php.aug"]) @provider.expects(:warning).once() @provider.expects(:debug).twice() @provider.print_load_errors('/augeas//error') end it "should find save errors and output to debug" do @augeas.expects(:match).with("/augeas//error[. = 'put_failed']").returns(["/augeas/files/foo/error"]) @augeas.expects(:match).with("/augeas/files/foo/error/*").returns(["/augeas/files/foo/error/path", "/augeas/files/foo/error/message"]) @augeas.expects(:get).with("/augeas/files/foo/error").returns("some_failure") @augeas.expects(:get).with("/augeas/files/foo/error/path").returns("/foo") @augeas.expects(:get).with("/augeas/files/foo/error/message").returns("Failed to...") @provider.expects(:debug).times(5) @provider.print_put_errors end end # Run initialisation tests of the real Augeas library to test our open_augeas # method. This relies on Augeas and ruby-augeas on the host to be # functioning. describe "augeas lib initialisation", :if => Puppet.features.augeas? do # Expect lenses for fstab and hosts it "should have loaded standard files by default" do aug = @provider.open_augeas aug.should_not == nil aug.match("/files/etc/fstab").should == ["/files/etc/fstab"] aug.match("/files/etc/hosts").should == ["/files/etc/hosts"] aug.match("/files/etc/test").should == [] end it "should report load errors to debug only" do @provider.expects(:print_load_errors).with(nil) aug = @provider.open_augeas aug.should_not == nil end # Only the file specified should be loaded it "should load one file if incl/lens used" do @resource[:incl] = "/etc/hosts" @resource[:lens] = "Hosts.lns" @provider.expects(:print_load_errors).with('/augeas//error') aug = @provider.open_augeas aug.should_not == nil aug.match("/files/etc/fstab").should == [] aug.match("/files/etc/hosts").should == ["/files/etc/hosts"] aug.match("/files/etc/test").should == [] end it "should also load lenses from load_path" do @resource[:load_path] = my_fixture_dir aug = @provider.open_augeas aug.should_not == nil aug.match("/files/etc/fstab").should == ["/files/etc/fstab"] aug.match("/files/etc/hosts").should == ["/files/etc/hosts"] aug.match("/files/etc/test").should == ["/files/etc/test"] end it "should also load lenses from pluginsync'd path" do Puppet[:libdir] = my_fixture_dir aug = @provider.open_augeas aug.should_not == nil aug.match("/files/etc/fstab").should == ["/files/etc/fstab"] aug.match("/files/etc/hosts").should == ["/files/etc/hosts"] aug.match("/files/etc/test").should == ["/files/etc/test"] end # Optimisations added for Augeas 0.8.2 or higher is available, see #7285 describe ">= 0.8.2 optimisations", :if => Puppet.features.augeas? && Facter.value(:augeasversion) && Puppet::Util::Package.versioncmp(Facter.value(:augeasversion), "0.8.2") >= 0 do it "should only load one file if relevant context given" do @resource[:context] = "/files/etc/fstab" @provider.expects(:print_load_errors).with('/augeas/files/etc/fstab//error') aug = @provider.open_augeas aug.should_not == nil aug.match("/files/etc/fstab").should == ["/files/etc/fstab"] aug.match("/files/etc/hosts").should == [] end it "should only load one lens from load_path if context given" do @resource[:context] = "/files/etc/test" @resource[:load_path] = my_fixture_dir @provider.expects(:print_load_errors).with('/augeas/files/etc/test//error') aug = @provider.open_augeas aug.should_not == nil aug.match("/files/etc/fstab").should == [] aug.match("/files/etc/hosts").should == [] aug.match("/files/etc/test").should == ["/files/etc/test"] end it "should load standard files if context isn't specific" do @resource[:context] = "/files/etc" @provider.expects(:print_load_errors).with(nil) aug = @provider.open_augeas aug.should_not == nil aug.match("/files/etc/fstab").should == ["/files/etc/fstab"] aug.match("/files/etc/hosts").should == ["/files/etc/hosts"] end it "should not optimise if the context is a complex path" do @resource[:context] = "/files/*[label()='etc']" @provider.expects(:print_load_errors).with(nil) aug = @provider.open_augeas aug.should_not == nil aug.match("/files/etc/fstab").should == ["/files/etc/fstab"] aug.match("/files/etc/hosts").should == ["/files/etc/hosts"] end end end describe "get_load_path" do it "should offer no load_path by default" do @provider.get_load_path(@resource).should == "" end it "should offer one path from load_path" do @resource[:load_path] = "/foo" @provider.get_load_path(@resource).should == "/foo" end it "should offer multiple colon-separated paths from load_path" do @resource[:load_path] = "/foo:/bar:/baz" @provider.get_load_path(@resource).should == "/foo:/bar:/baz" end it "should offer multiple paths in array from load_path" do @resource[:load_path] = ["/foo", "/bar", "/baz"] @provider.get_load_path(@resource).should == "/foo:/bar:/baz" end it "should offer pluginsync augeas/lenses subdir" do Puppet[:libdir] = my_fixture_dir @provider.get_load_path(@resource).should == "#{my_fixture_dir}/augeas/lenses" end it "should offer both pluginsync and load_path paths" do Puppet[:libdir] = my_fixture_dir @resource[:load_path] = ["/foo", "/bar", "/baz"] @provider.get_load_path(@resource).should == "/foo:/bar:/baz:#{my_fixture_dir}/augeas/lenses" end end end