diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index 5fa5ab409..e9471b1be 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -1,259 +1,277 @@ #-- # Copyright (C) 2008 Red Hat Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Author: Bryan Kearney require 'augeas' if Puppet.features.augeas? Puppet::Type.type(:augeas).provide(:augeas) do include Puppet::Util confine :true => Puppet.features.augeas? has_features :parse_commands, :need_to_run?,:execute_changes SAVE_NOOP = "noop" SAVE_OVERWRITE = "overwrite" 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) commands = Array.new() if data.is_a?(String) data.each_line do |line| cmd_array = Array.new() - tokens = line.split(" ") - cmd = tokens.shift() - file = tokens.shift() - other = tokens.join(" ") + single = line.index("'") + double = line.index('"') + tokens = nil + delim = " " + if ((single != nil) or (double != nil)) + single = 99999 if single == nil + double = 99999 if double == nil + delim = '"' if double < single + delim = "'" if single < double + end + tokens = line.split(delim) + # If the length of tokens is 2, thn that means the pattern was + # command file "some text", therefore we need to re-split + # the first line + if tokens.length == 2 + tokens = (tokens[0].split(" ")) << tokens[1] + end + cmd = tokens.shift().strip() + delim = "" if delim == " " + file = tokens.shift().strip() + other = tokens.join(" ").strip() cmd_array << cmd if !cmd.nil? cmd_array << file if !file.nil? cmd_array << other if other != "" commands << cmd_array end elsif data.is_a?(Array) data.each do |datum| commands.concat(parse_commands(datum)) end end return commands end + def open_augeas if (@aug.nil?) flags = 0 (flags = 1 << 2 ) if self.resource[:type_check] == :true root = self.resource[:root] load_path = self.resource[:load_path] debug("Opening augeas with root #{root}, lens path #{load_path}, flags #{flags}") @aug = Augeas.open(root, load_path,flags) if (self.get_augeas_version() >= "0.3.6") debug("Augeas version #{self.get_augeas_version()} is installed") end end @aug 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) || '' unless result.nil? case comparator when "!=": return_value = true if !(result == arg) when "=~": regex = Regexp.new(arg) loc = result=~ regex return_value = true if ! loc.nil? else return_value = true if (result.send(comparator, arg)) end 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 < 4 cmd = cmd_array.shift() path = cmd_array.shift() verb = cmd_array.shift() #Get the values from augeas result = @aug.match(path) || '' # Now do the work if (!result.nil?) case verb when "size": fail("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length != 2 comparator = cmd_array.shift() arg = cmd_array.shift().to_i return_value = true if (result.size.send(comparator, arg)) when "include": arg = cmd_array.join(" ") return_value = true if result.include?(arg) when "==": begin arg = cmd_array.join(" ") new_array = eval arg return_value = true if result == new_array rescue fail("Invalid array in command: #{cmd_array.join(" ")}") end end end return_value end def get_augeas_version return @aug.get("/augeas/version") || "" end def set_augeas_save_mode(mode) return @aug.set("/augeas/save", mode) end def files_changed? saved_files = @aug.match("/augeas/events/saved") return saved_files.size() > 0 end # Determines if augeas acutally needs to run. def need_to_run? self.open_augeas() return_value = true filter = resource[:onlyif] unless (filter == "") cmd_array = filter.split command = cmd_array[0]; cmd_array[1]= File.join(resource[:context], cmd_array[1]) begin data = nil case command when "get" then return_value = process_get(cmd_array) when "match" then return_value = process_match(cmd_array) end rescue Exception => e fail("Error sending command '#{command}' with params #{cmd_array[1..-1].inspect}/#{e.message}") end end # If we have a verison of augeas which is at least 0.3.6 then we # can make the changes now, see if changes were made, and # actually do the save. if ((return_value) and (self.get_augeas_version() >= "0.3.6")) debug("Will attempt to save and only run if files changed") self.set_augeas_save_mode(SAVE_NOOP) self.do_execute_changes() save_result = @aug.save() saved_files = @aug.match("/augeas/events/saved") if ((save_result) and (not files_changed?)) debug("Skipping becuase no files were changed") return_value = false else debug("Files changed, should execute") end end return return_value end def execute_changes # if we have version 0.3.6 or greater we have already executed # the changes. We just need to save them. If not, do the changes if (self.get_augeas_version() >= "0.3.6") self.set_augeas_save_mode(SAVE_OVERWRITE) else self.do_execute_changes() end success = @aug.save() if (success != true) fail("Save failed with return code #{success}") end return :executed end # Actually execute the augeas changes. def do_execute_changes commands = resource[:changes] context = resource[:context] 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": cmd_array[0]=File.join(context, cmd_array[0]) debug("sending command '#{command}' with params #{cmd_array.inspect}") @aug.set(cmd_array[0], cmd_array[1]) when "rm", "remove": cmd_array[0]=File.join(context, cmd_array[0]) debug("sending command '#{command}' with params #{cmd_array.inspect}") @aug.rm(cmd_array[0]) when "clear": cmd_array[0]=File.join(context, cmd_array[0]) debug("sending command '#{command}' with params #{cmd_array.inspect}") @aug.clear(cmd_array[0]) when "insert", "ins" if cmd_array.size < 3 fail("ins requires 3 parameters") end label = cmd_array[0] where = cmd_array[1] path = File.join(context, 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()}") @aug.insert(path, label, before) else fail("Command '#{command}' is not supported") end 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.rb b/spec/unit/provider/augeas/augeas.rb index 3ce64576e..e05812d78 100644 --- a/spec/unit/provider/augeas/augeas.rb +++ b/spec/unit/provider/augeas/augeas.rb @@ -1,255 +1,335 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' provider_class = Puppet::Type.type(:augeas).provider(:augeas) describe provider_class do describe "command parsing" do it "should break apart a single line into three tokens" do provider = provider_class.new() 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 == "/Jar/Jar" tokens[0][2].should == "Binks" end it "should break apart a multiple line into six tokens" do provider = provider_class.new() tokens = provider.parse_commands("set /Jar/Jar Binks\nrm anakin skywalker") tokens.size.should == 2 tokens[0].size.should == 3 tokens[1].size.should == 3 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" tokens[1][2].should == "skywalker" end it "should handle arrays" do provider = provider_class.new() commands = ["set /Jar/Jar Binks", "rm anakin skywalker"] tokens = provider.parse_commands(commands) tokens.size.should == 2 tokens[0].size.should == 3 tokens[1].size.should == 3 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" tokens[1][2].should == "skywalker" end 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 and and single ticks" do + provider = provider_class.new() + 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 == "Jar Jar" + tokens[0][2].should == "Binks" + end + + it "should accept spaces in the value and and single ticks" 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 and and double ticks" do + provider = provider_class.new() + 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 == 'Jar Jar' + tokens[0][2].should == 'Binks' + end + + it "should accept spaces in the value and and double ticks" 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 mixed ticks" do + provider = provider_class.new() + tokens = provider.parse_commands('set "Jar Jar" "Some \'Test\'"') + tokens.size.should == 1 + tokens[0].size.should == 3 + tokens[0][0].should == "set" + tokens[0][1].should == 'Jar Jar' + tokens[0][2].should == "Some \'Test\'" + end + + it "should accept only the last value using ticks" 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 only the first value using ticks" do + provider = provider_class.new() + tokens = provider.parse_commands('set "Jar Jar" 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 == "copilot" + end + + it "should accept only the first value using ticks and the last values being concatenated" 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 end describe "get filters" do before do augeas_stub = stub("augeas", :get => "value") @provider = provider_class.new() @provider.aug= augeas_stub 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 = stub("augeas", :match => ["set", "of", "values"]) @provider = provider_class.new() @provider.aug= augeas_stub 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 = ["get", "fake value", "include", "values"] @provider.process_match(command).should == true end it "should return false for includes non match" do command = ["get", "fake value", "include", "JarJar"] @provider.process_match(command).should == false end it "should return true for an array match" do command = ["get", "fake value", "==", "['set', 'of', 'values']"] @provider.process_match(command).should == true end it "should return false for an array non match" do command = ["get", "fake value", "==", "['this', 'should', 'not', 'match']"] @provider.process_match(command).should == false end end describe "legacy need to run" do it "should handle no filters" do resource = stub("resource", :[] => "") augeas_stub = stub("augeas", :match => ["set", "of", "values"]) provider = provider_class.new(resource) provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == true end it "should return true when a get filter matches" do resource = stub("resource", :[] => "get path == value") provider = provider_class.new(resource) augeas_stub = stub("augeas", :get => "value") provider.aug= augeas_stub provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == true end it "should return false when a get filter does not match" do resource = stub("resource", :[] => "get path == another value") provider = provider_class.new(resource) augeas_stub = stub("augeas", :get => "value") provider.aug= augeas_stub provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == false end it "should return true when a match filter matches" do resource = stub("resource", :[] => "match path size == 3") provider = provider_class.new(resource) augeas_stub = stub("augeas", :match => ["set", "of", "values"]) provider.aug= augeas_stub provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == true end it "should return false when a match filter does not match" do resource = stub("resource", :[] => "match path size == 2") provider = provider_class.new(resource) augeas_stub = stub("augeas", :match => ["set", "of", "values"]) provider.aug= augeas_stub provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == false end end describe "legacy augeas integration" do before do @resource = stub("resource") @provider = provider_class.new(@resource) @augeas = stub("augeas") @provider.aug= @augeas @provider.stubs(:get_augeas_version).returns("0.3.5") end it "should handle set commands" do command = [["set", "/Jar/Jar", "Binks"]] context = "/some/path" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:set).with("/some/path/Jar/Jar", "Binks") @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end it "should handle rm commands" do command = [["rm", "/Jar/Jar"]] context = "" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:rm).with("/Jar/Jar") @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end it "should handle remove commands" do command = [["remove", "Jar/Jar"]] context = "" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:rm).with("/Jar/Jar") @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end it "should handle clear commands" do command = [["clear", "/Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:clear).with("/foo/Jar/Jar") @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end it "should handle ins commands with before" do command = [["ins", "Binks", "before /Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", true) @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end it "should handle ins commands with before" do command = [["ins", "Binks", "after /Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", false) @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end it "should handle ins with no context" do command = [["ins", "Binks", "after /Jar/Jar"]] context = "" # this is the default @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:insert).with("/Jar/Jar", "Binks", false) @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end it "should handle multiple commands" do command = [["ins", "Binks", "after /Jar/Jar"], ["clear", "/Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", false) @augeas.expects(:clear).with("/foo/Jar/Jar") @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end end end