aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/puppet/feature/augeas.rb3
-rw-r--r--lib/puppet/provider/augeas/augeas.rb573
-rw-r--r--lib/puppet/type/augeas.rb211
-rw-r--r--spec/acceptance/hosts.rb78
-rw-r--r--spec/acceptance/puppet.rb46
-rw-r--r--spec/acceptance/services.rb73
-rw-r--r--spec/fixtures/unit/provider/augeas/augeas/augeas/lenses/test.aug13
-rw-r--r--spec/fixtures/unit/provider/augeas/augeas/etc/fstab10
-rw-r--r--spec/fixtures/unit/provider/augeas/augeas/etc/hosts6
-rw-r--r--spec/fixtures/unit/provider/augeas/augeas/etc/test3
-rw-r--r--spec/fixtures/unit/provider/augeas/augeas/test.aug13
-rw-r--r--spec/unit/provider/augeas/augeas_spec.rb1033
-rw-r--r--spec/unit/type/augeas_spec.rb119
13 files changed, 2181 insertions, 0 deletions
diff --git a/lib/puppet/feature/augeas.rb b/lib/puppet/feature/augeas.rb
new file mode 100644
index 0000000..e22f53b
--- /dev/null
+++ b/lib/puppet/feature/augeas.rb
@@ -0,0 +1,3 @@
+require 'puppet/util/feature'
+
+Puppet.features.add(:augeas, :libs => ["augeas"])
diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb
new file mode 100644
index 0000000..6c9102b
--- /dev/null
+++ b/lib/puppet/provider/augeas/augeas.rb
@@ -0,0 +1,573 @@
+#
+# Copyright 2011 Bryan Kearney <bkearney@redhat.com>
+#
+# 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
+#
+# https://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 ],
+ "touch" => [ :path ],
+ "mv" => [ :path, :path ],
+ "rename" => [ :path, :string ],
+ "insert" => [ :string, :string, :path ],
+ "get" => [ :path, :comparator, :string ],
+ "values" => [ :path, :glob ],
+ "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}") % { cmd: 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}") % { narg: narg, cmd: 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}]|(\\.))*/)
+ # Unescape the delimiter so it's actually possible to have a
+ # literal delim inside the string. We only unescape the
+ # delimeter and not every backslash-escaped character so that
+ # things like escaped spaces '\ ' get passed through because
+ # Augeas needs to see them. If we unescaped them, too, users
+ # would be forced to double-escape them
+ argline.last.gsub!(/\\(#{delim})/, '\1')
+ sc.getch
+ else
+ argline << sc.scan(/[^\s]+/)
+ end
+ fail(_("missing string argument %{narg} for %{cmd}") % { narg: narg, cmd: cmd }) unless argline[-1]
+ elsif f == :comparator
+ argline << sc.scan(/(==|!=|=~|<=|>=|<|>)/)
+ unless argline[-1]
+ puts sc.rest
+ fail(_("invalid comparator for command %{cmd}") % { cmd: 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]
+
+ 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 Integer
+ 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}") % { cmd: cmd_array.join(" ") }) if cmd_array.length < 4
+ _ = 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 values filters. Returns
+ # true if there is a matched value, false if otherwise
+ def process_values(cmd_array)
+ return_value = false
+
+ #validate and tear apart the command
+ fail(_("Invalid command: %{cmd}") % { cmd: cmd_array.join(" ") }) if cmd_array.length < 3
+ _ = 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 match paths from augeas
+ result = @aug.match(path) || []
+ fail(_("Error trying to get path '%{path}'") % { path: path }) if (result == -1)
+
+ #Get the values of the match paths from augeas
+ values = result.collect{|r| @aug.get(r)}
+
+ case verb
+ when "include"
+ arg = clause_array.shift
+ return_value = values.include?(arg)
+ when "not_include"
+ arg = clause_array.shift
+ return_value = !values.include?(arg)
+ when "=="
+ begin
+ arg = clause_array.shift
+ new_array = eval arg
+ return_value = (values == new_array)
+ rescue
+ fail(_("Invalid array in command: %{cmd}") % { cmd: cmd_array.join(" ") })
+ end
+ when "!="
+ begin
+ arg = clause_array.shift
+ new_array = eval arg
+ return_value = (values != new_array)
+ rescue
+ fail(_("Invalid array in command: %{cmd}") % { cmd: cmd_array.join(" ") })
+ 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}") % { cmd: cmd_array.join(" ") }) if cmd_array.length < 3
+ _ = 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}'") % { path: path }) if (result == -1)
+
+ # Now do the work
+ case verb
+ when "size"
+ fail(_("Invalid command: %{cmd}") % { cmd: 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}") % { cmd: 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}") % { cmd: 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 "values"; return_value = process_values(cmd_array)
+ when "match"; return_value = process_match(cmd_array)
+ end
+ rescue StandardError => e
+ fail(_("Error sending command '%{command}' with params %{param}/%{message}") % { command: command, param: cmd_array[1..-1].inspect, message: e.message })
+ end
+ end
+
+ unless force
+ # If we have a version 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] && @resource[:show_diff]
+ 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}") % { value0: 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 %{params}") % { command: command, 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 %{params}") % { command: command, params: cmd_array.inspect }) if (rv == -1)
+ else
+ fail(_("command '%{command}' not supported in installed version of ruby-augeas") % { command: command })
+ 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 %{params}") % { command: command, 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 %{params}") % { command: command, 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 %{params}") % { command: command, params: cmd_array.inspect }) if (!rv)
+ else
+ fail(_("command '%{command}' not supported in installed version of ruby-augeas") % { command: command })
+ end
+ when "touch"
+ debug("sending command '#{command}' (match, set) with params #{cmd_array.inspect}")
+ if aug.match(cmd_array[0]).empty?
+ rv = aug.clear(cmd_array[0])
+ fail(_("Error sending command '%{command}' with params %{params}") % { command: command, params: cmd_array.inspect }) if (!rv)
+ 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") % { where: where })
+ end
+ debug("sending command '#{command}' with params #{[label, where, path].inspect}")
+ rv = aug.insert(path, label, before)
+ fail(_("Error sending command '%{command}' with params %{params}") % { command: command, 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 %{params}") % { command: command, 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 %{params}") % { command: command, 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 %{params}") % { command: command, params: cmd_array.inspect }) if (rv == -1)
+ when "rename"
+ debug("sending command '#{command}' with params #{cmd_array.inspect}")
+ rv = aug.rename(cmd_array[0], cmd_array[1])
+ fail(_("Error sending command '%{command}' with params %{params}") % { command: command, params: cmd_array.inspect }) if (rv == -1)
+ else fail(_("Command '%{command}' is not supported") % { command: command })
+ end
+ rescue StandardError => e
+ fail(_("Error sending command '%{command}' with params %{params}/%{message}") % { command: command, params: cmd_array.inspect, message: e.message })
+ end
+ end
+ end
+end
diff --git a/lib/puppet/type/augeas.rb b/lib/puppet/type/augeas.rb
new file mode 100644
index 0000000..49be7b9
--- /dev/null
+++ b/lib/puppet/type/augeas.rb
@@ -0,0 +1,211 @@
+#
+# Copyright 2011 Bryan Kearney <bkearney@redhat.com>
+#
+# 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
+#
+# https://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 'puppet/parameter/boolean'
+
+Puppet::Type.newtype(:augeas) do
+ include Puppet::Util
+
+ feature :parse_commands, "Parse the command string"
+ feature :need_to_run?, "If the command should run"
+ feature :execute_changes, "Actually make the changes"
+
+ @doc = <<-'EOT'
+ Apply a change or an array of changes to the filesystem
+ using the augeas tool.
+
+ Requires:
+
+ - [Augeas](http://www.augeas.net)
+ - The ruby-augeas bindings
+
+ Sample usage with a string:
+
+ augeas{"test1" :
+ context => "/files/etc/sysconfig/firstboot",
+ changes => "set RUN_FIRSTBOOT YES",
+ onlyif => "match other_value size > 0",
+ }
+
+ Sample usage with an array and custom lenses:
+
+ augeas{"jboss_conf":
+ context => "/files",
+ changes => [
+ "set etc/jbossas/jbossas.conf/JBOSS_IP $ipaddress",
+ "set etc/jbossas/jbossas.conf/JAVA_HOME /usr",
+ ],
+ load_path => "$/usr/share/jbossas/lenses",
+ }
+
+ EOT
+
+ newparam (:name) do
+ desc "The name of this task. Used for uniqueness."
+ isnamevar
+ end
+
+ newparam (:context) do
+ desc "Optional context path. This value is prepended to the paths of all
+ changes if the path is relative. If the `incl` parameter is set,
+ defaults to `/files + incl`; otherwise, defaults to the empty string."
+ defaultto ""
+ munge do |value|
+ if value.empty? and resource[:incl]
+ "/files" + resource[:incl]
+ else
+ value
+ end
+ end
+ end
+
+ newparam (:onlyif) do
+ desc "Optional augeas command and comparisons to control the execution of this type.
+
+ Note: `values` is not an actual augeas API command. It calls `match` to retrieve an array of paths
+ in <MATCH_PATH> and then `get` to retrieve the values from each of the returned paths.
+
+ Supported onlyif syntax:
+
+ * `get <AUGEAS_PATH> <COMPARATOR> <STRING>`
+ * `values <MATCH_PATH> include <STRING>`
+ * `values <MATCH_PATH> not_include <STRING>`
+ * `values <MATCH_PATH> == <AN_ARRAY>`
+ * `values <MATCH_PATH> != <AN_ARRAY>`
+ * `match <MATCH_PATH> size <COMPARATOR> <INT>`
+ * `match <MATCH_PATH> include <STRING>`
+ * `match <MATCH_PATH> not_include <STRING>`
+ * `match <MATCH_PATH> == <AN_ARRAY>`
+ * `match <MATCH_PATH> != <AN_ARRAY>`
+
+ where:
+
+ * `AUGEAS_PATH` is a valid path scoped by the context
+ * `MATCH_PATH` is a valid match syntax scoped by the context
+ * `COMPARATOR` is one of `>, >=, !=, ==, <=,` or `<`
+ * `STRING` is a string
+ * `INT` is a number
+ * `AN_ARRAY` is in the form `['a string', 'another']`"
+ defaultto ""
+ end
+
+
+ newparam(:changes) do
+ desc "The changes which should be applied to the filesystem. This
+ can be a command or an array of commands. The following commands are supported:
+
+ * `set <PATH> <VALUE>` --- Sets the value `VALUE` at location `PATH`
+ * `setm <PATH> <SUB> <VALUE>` --- Sets multiple nodes (matching `SUB` relative to `PATH`) to `VALUE`
+ * `rm <PATH>` --- Removes the node at location `PATH`
+ * `remove <PATH>` --- Synonym for `rm`
+ * `clear <PATH>` --- Sets the node at `PATH` to `NULL`, creating it if needed
+ * `clearm <PATH> <SUB>` --- Sets multiple nodes (matching `SUB` relative to `PATH`) to `NULL`
+ * `touch <PATH>` --- Creates `PATH` with the value `NULL` if it does not exist
+ * `ins <LABEL> (before|after) <PATH>` --- Inserts an empty node `LABEL` either before or after `PATH`.
+ * `insert <LABEL> <WHERE> <PATH>` --- Synonym for `ins`
+ * `mv <PATH> <OTHER PATH>` --- Moves a node at `PATH` to the new location `OTHER PATH`
+ * `move <PATH> <OTHER PATH>` --- Synonym for `mv`
+ * `rename <PATH> <LABEL>` --- Rename a node at `PATH` to a new `LABEL`
+ * `defvar <NAME> <PATH>` --- Sets Augeas variable `$NAME` to `PATH`
+ * `defnode <NAME> <PATH> <VALUE>` --- Sets Augeas variable `$NAME` to `PATH`, creating it with `VALUE` if needed
+
+ If the `context` parameter is set, that value is prepended to any relative `PATH`s."
+ end
+
+
+ newparam(:root) do
+ desc "A file system path; all files loaded by Augeas are loaded underneath `root`."
+ defaultto "/"
+ end
+
+ newparam(:load_path) do
+ desc "Optional colon-separated list or array of directories; these directories are searched for schema definitions. The agent's `$libdir/augeas/lenses` path will always be added to support pluginsync."
+ defaultto ""
+ end
+
+ newparam(:force) do
+ desc "Optional command to force the augeas type to execute even if it thinks changes
+ will not be made. This does not override the `onlyif` parameter."
+
+ defaultto false
+ end
+
+ newparam(:type_check) do
+ desc "Whether augeas should perform typechecking. Defaults to false."
+ newvalues(:true, :false)
+
+ defaultto :false
+ end
+
+ newparam(:lens) do
+ desc "Use a specific lens, such as `Hosts.lns`. When this parameter is set, you
+ must also set the `incl` parameter to indicate which file to load.
+ The Augeas documentation includes [a list of available lenses](http://augeas.net/stock_lenses.html)."
+ end
+
+ newparam(:incl) do
+ desc "Load only a specific file, such as `/etc/hosts`. This can greatly speed
+ up the execution the resource. When this parameter is set, you must also
+ set the `lens` parameter to indicate which lens to use."
+ end
+
+ validate do
+ has_lens = !self[:lens].nil?
+ has_incl = !self[:incl].nil?
+ self.fail _("You must specify both the lens and incl parameters, or neither.") if has_lens != has_incl
+ end
+
+ newparam(:show_diff, :boolean => true, :parent => Puppet::Parameter::Boolean) do
+ desc "Whether to display differences when the file changes, defaulting to
+ true. This parameter is useful for files that may contain passwords or
+ other secret data, which might otherwise be included in Puppet reports or
+ other insecure outputs. If the global `show_diff` setting
+ is false, then no diffs will be shown even if this parameter is true."
+
+ defaultto :true
+ end
+
+ # This is the actual meat of the code. It forces
+ # augeas to be run and fails or not based on the augeas return
+ # code.
+ newproperty(:returns) do |property|
+ include Puppet::Util
+ desc "The expected return code from the augeas command. Should not be set."
+
+ defaultto 0
+
+ # Make output a bit prettier
+ def change_to_s(currentvalue, newvalue)
+ _("executed successfully")
+ end
+
+ # if the onlyif resource is provided, then the value is parsed.
+ # a return value of 0 will stop execution because it matches the
+ # default value.
+ def retrieve
+ if @resource.provider.need_to_run?()
+ :need_to_run
+ else
+ 0
+ end
+ end
+
+ # Actually execute the command.
+ def sync
+ @resource.provider.execute_changes
+ end
+ end
+
+end
diff --git a/spec/acceptance/hosts.rb b/spec/acceptance/hosts.rb
new file mode 100644
index 0000000..a7f526a
--- /dev/null
+++ b/spec/acceptance/hosts.rb
@@ -0,0 +1,78 @@
+test_name "Augeas hosts file" do
+
+tag 'risk:medium',
+ 'audit:medium',
+ 'audit:acceptance',
+ 'audit:refactor' # move to puppet types test directory, this is not testing puppet apply
+ # reduce to a single manifest and apply
+
+skip_test 'requires augeas which is included in AIO' if @options[:type] != 'aio'
+
+ confine :except, :platform => [
+ 'windows',
+ 'cisco_ios', # PUP-7380
+ ]
+ confine :to, {}, hosts.select { |host| ! host[:roles].include?('master') }
+
+ step "Backup the hosts file" do
+ on hosts, 'cp /etc/hosts /tmp/hosts.bak'
+ end
+
+ # We have a begin/ensure block here to clean up the hosts file in case
+ # of test failure.
+ begin
+
+ step "Create an entry in the hosts file" do
+ manifest = <<EOF
+augeas { 'add_hosts_entry':
+ context => '/files/etc/hosts',
+ incl => '/etc/hosts',
+ lens => 'Hosts.lns',
+ changes => [
+ 'set 01/ipaddr 192.168.0.1',
+ 'set 01/canonical pigiron.example.com',
+ 'set 01/alias[1] pigiron',
+ 'set 01/alias[2] piggy'
+ ]
+}
+EOF
+ on hosts, puppet_apply('--verbose'), :stdin => manifest
+ on hosts, "fgrep '192.168.0.1\tpigiron.example.com pigiron piggy' /etc/hosts"
+ end
+
+ step "Modify an entry in the hosts file" do
+ manifest = <<EOF
+augeas { 'mod_hosts_entry':
+ context => '/files/etc/hosts',
+ incl => '/etc/hosts',
+ lens => 'Hosts.lns',
+ changes => [
+ 'set *[canonical = "pigiron.example.com"]/alias[last()+1] oinker'
+ ]
+}
+EOF
+
+ on hosts, puppet_apply('--verbose'), :stdin => manifest
+ on hosts, "fgrep '192.168.0.1\tpigiron.example.com pigiron piggy oinker' /etc/hosts"
+ end
+
+ step "Remove an entry from the hosts file" do
+ manifest = <<EOF
+augeas { 'del_hosts_entry':
+ context => '/files/etc/hosts',
+ incl => '/etc/hosts',
+ lens => 'Hosts.lns',
+ changes => [
+ 'rm *[canonical = "pigiron.example.com"]'
+ ]
+}
+EOF
+
+ on hosts, puppet_apply('--verbose'), :stdin => manifest
+ on hosts, "fgrep 'pigiron.example.com' /etc/hosts", :acceptable_exit_codes => [1]
+ end
+
+ ensure
+ on hosts, 'cat /tmp/hosts.bak > /etc/hosts && rm /tmp/hosts.bak'
+ end
+end
diff --git a/spec/acceptance/puppet.rb b/spec/acceptance/puppet.rb
new file mode 100644
index 0000000..10fb159
--- /dev/null
+++ b/spec/acceptance/puppet.rb
@@ -0,0 +1,46 @@
+test_name "Augeas puppet configuration" do
+
+ tag 'risk:medium',
+ 'audit:medium',
+ 'audit:acceptance',
+ 'audit:refactor' # move to types test dir
+
+ skip_test 'requires augeas which is included in AIO' if @options[:type] != 'aio'
+
+ confine :except, :platform => 'windows'
+ confine :to, {}, hosts.select { |host| ! host[:roles].include?('master') }
+
+ teardown do
+ agents.each do |agent|
+ on agent, "cat /tmp/puppet.conf.bak > #{agent.puppet['confdir']}/puppet.conf && rm /tmp/puppet.conf.bak"
+ end
+ end
+
+ agents.each do |agent|
+ step "Backup the puppet config" do
+ on agent, "mv #{agent.puppet['confdir']}/puppet.conf /tmp/puppet.conf.bak"
+ end
+ step "Create a new puppet config that has a master and agent section" do
+ puppet_conf = <<-CONF
+ [main]
+ CONF
+ on agent, "echo \"#{puppet_conf}\" >> #{agent.puppet['confdir']}/puppet.conf"
+ end
+
+ step "Modify the puppet.conf file" do
+ manifest = <<-EOF
+ augeas { 'puppet agent noop mode':
+ context => "/files#{agent.puppet['confdir']}/puppet.conf/agent",
+ incl => "/etc/puppetlabs/puppet/puppet.conf",
+ lens => 'Puppet.lns',
+ changes => 'set noop true',
+ }
+ EOF
+ on agent, puppet_apply('--verbose'), :stdin => manifest
+
+ on agent, "grep 'noop=true' #{agent.puppet['confdir']}/puppet.conf"
+ end
+
+ end
+
+end
diff --git a/spec/acceptance/services.rb b/spec/acceptance/services.rb
new file mode 100644
index 0000000..ec87316
--- /dev/null
+++ b/spec/acceptance/services.rb
@@ -0,0 +1,73 @@
+test_name "Augeas services file" do
+
+ tag 'risk:medium',
+ 'audit:medium',
+ 'audit:acceptance',
+ 'audit:refactor' # move to types test dir
+ # use single manifest/apply
+
+ skip_test 'requires augeas which is included in AIO' if @options[:type] != 'aio'
+
+ confine :except, :platform => 'windows'
+ confine :except, :platform => 'osx'
+ confine :to, {}, hosts.select { |host| ! host[:roles].include?('master') }
+
+ step "Backup the services file" do
+ on hosts, "cp /etc/services /tmp/services.bak"
+ end
+
+ begin
+ step "Add an entry to the services file" do
+ manifest = <<EOF
+augeas { 'add_services_entry':
+ context => '/files/etc/services',
+ incl => '/etc/services',
+ lens => 'Services.lns',
+ changes => [
+ 'ins service-name after service-name[last()]',
+ 'set service-name[last()] "Doom"',
+ 'set service-name[. = "Doom"]/port "666"',
+ 'set service-name[. = "Doom"]/protocol "udp"'
+ ]
+}
+EOF
+
+ on hosts, puppet_apply('--verbose'), :stdin => manifest
+ on hosts, "fgrep 'Doom 666/udp' /etc/services"
+ end
+
+ step "Change the protocol to udp" do
+ manifest = <<EOF
+augeas { 'change_service_protocol':
+ context => '/files/etc/services',
+ incl => '/etc/services',
+ lens => 'Services.lns',
+ changes => [
+ 'set service-name[. = "Doom"]/protocol "tcp"'
+ ]
+}
+EOF
+
+ on hosts, puppet_apply('--verbose'), :stdin => manifest
+ on hosts, "fgrep 'Doom 666/tcp' /etc/services"
+ end
+
+ step "Remove the services entry" do
+ manifest = <<EOF
+augeas { 'del_service_entry':
+ context => '/files/etc/services',
+ incl => '/etc/services',
+ lens => 'Services.lns',
+ changes => [
+ 'rm service-name[. = "Doom"]'
+ ]
+}
+EOF
+
+ on hosts, puppet_apply('--verbose'), :stdin => manifest
+ on hosts, "fgrep 'Doom 666/tcp' /etc/services", :acceptable_exit_codes => [1]
+ end
+ ensure
+ on hosts, "mv /tmp/services.bak /etc/services"
+ end
+end
diff --git a/spec/fixtures/unit/provider/augeas/augeas/augeas/lenses/test.aug b/spec/fixtures/unit/provider/augeas/augeas/augeas/lenses/test.aug
new file mode 100644
index 0000000..bea707e
--- /dev/null
+++ b/spec/fixtures/unit/provider/augeas/augeas/augeas/lenses/test.aug
@@ -0,0 +1,13 @@
+(*
+Simple lens, written to be distributed with Puppet unit tests.
+
+Author: Dominic Cleal <dcleal@redhat.com>
+
+About: License:
+ This file is licensed under the Apache 2.0 licence, like the rest of Puppet.
+*)
+
+module Test = autoload xfm
+let lns = [ seq "line" . store /[^\n]+/ . del "\n" "\n" ]*
+let filter = incl "/etc/test"
+let xfm = transform lns filter
diff --git a/spec/fixtures/unit/provider/augeas/augeas/etc/fstab b/spec/fixtures/unit/provider/augeas/augeas/etc/fstab
new file mode 100644
index 0000000..ddbd8ff
--- /dev/null
+++ b/spec/fixtures/unit/provider/augeas/augeas/etc/fstab
@@ -0,0 +1,10 @@
+/dev/vg00/lv00 / ext3 defaults 1 1
+LABEL=/boot /boot ext3 defaults 1 2
+devpts /dev/pts devpts gid=5,mode=620 0 0
+tmpfs /dev/shm tmpfs defaults 0 0
+/dev/vg00/home /home ext3 defaults 1 2
+proc /proc proc defaults 0 0
+sysfs /sys sysfs defaults 0 0
+/dev/vg00/local /local ext3 defaults 1 2
+/dev/vg00/images /var/lib/xen/images ext3 defaults 1 2
+/dev/vg00/swap swap swap defaults 0 0
diff --git a/spec/fixtures/unit/provider/augeas/augeas/etc/hosts b/spec/fixtures/unit/provider/augeas/augeas/etc/hosts
new file mode 100644
index 0000000..44cd9da
--- /dev/null
+++ b/spec/fixtures/unit/provider/augeas/augeas/etc/hosts
@@ -0,0 +1,6 @@
+# Do not remove the following line, or various programs
+# that require network functionality will fail.
+127.0.0.1 localhost.localdomain localhost galia.watzmann.net galia
+#172.31.122.254 granny.watzmann.net granny puppet
+#172.31.122.1 galia.watzmann.net galia
+172.31.122.14 orange.watzmann.net orange
diff --git a/spec/fixtures/unit/provider/augeas/augeas/etc/test b/spec/fixtures/unit/provider/augeas/augeas/etc/test
new file mode 100644
index 0000000..86e041d
--- /dev/null
+++ b/spec/fixtures/unit/provider/augeas/augeas/etc/test
@@ -0,0 +1,3 @@
+foo
+bar
+baz
diff --git a/spec/fixtures/unit/provider/augeas/augeas/test.aug b/spec/fixtures/unit/provider/augeas/augeas/test.aug
new file mode 100644
index 0000000..bea707e
--- /dev/null
+++ b/spec/fixtures/unit/provider/augeas/augeas/test.aug
@@ -0,0 +1,13 @@
+(*
+Simple lens, written to be distributed with Puppet unit tests.
+
+Author: Dominic Cleal <dcleal@redhat.com>
+
+About: License:
+ This file is licensed under the Apache 2.0 licence, like the rest of Puppet.
+*)
+
+module Test = autoload xfm
+let lns = [ seq "line" . store /[^\n]+/ . del "\n" "\n" ]*
+let filter = incl "/etc/test"
+let xfm = transform lns filter
diff --git a/spec/unit/provider/augeas/augeas_spec.rb b/spec/unit/provider/augeas/augeas_spec.rb
new file mode 100644
index 0000000..51a734d
--- /dev/null
+++ b/spec/unit/provider/augeas/augeas_spec.rb
@@ -0,0 +1,1033 @@
+#! /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
+
+ def my_fixture_dir
+ File.expand_path(File.join(File.dirname(__FILE__), '../../../fixtures/unit/provider/augeas/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")
+ expect(tokens.size).to eq(1)
+ expect(tokens[0].size).to eq(3)
+ expect(tokens[0][0]).to eq("set")
+ expect(tokens[0][1]).to eq("/context/Jar/Jar")
+ expect(tokens[0][2]).to eq("Binks")
+ end
+
+ it "should break apart a multiple line into six tokens" do
+ tokens = @provider.parse_commands("set /Jar/Jar Binks\nrm anakin")
+ expect(tokens.size).to eq(2)
+ expect(tokens[0].size).to eq(3)
+ expect(tokens[1].size).to eq(2)
+ expect(tokens[0][0]).to eq("set")
+ expect(tokens[0][1]).to eq("/Jar/Jar")
+ expect(tokens[0][2]).to eq("Binks")
+ expect(tokens[1][0]).to eq("rm")
+ expect(tokens[1][1]).to eq("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 ")
+ expect(tokens.size).to eq(2)
+ expect(tokens[0].size).to eq(3)
+ expect(tokens[1].size).to eq(2)
+ expect(tokens[0][0]).to eq("set")
+ expect(tokens[0][1]).to eq("/Jar/Jar")
+ expect(tokens[0][2]).to eq("Binks")
+ expect(tokens[1][0]).to eq("rm")
+ expect(tokens[1][1]).to eq("anakin")
+ end
+
+ it "should handle arrays" do
+ @resource[:context] = "/foo/"
+ commands = ["set /Jar/Jar Binks", "rm anakin"]
+ tokens = @provider.parse_commands(commands)
+ expect(tokens.size).to eq(2)
+ expect(tokens[0].size).to eq(3)
+ expect(tokens[1].size).to eq(2)
+ expect(tokens[0][0]).to eq("set")
+ expect(tokens[0][1]).to eq("/Jar/Jar")
+ expect(tokens[0][2]).to eq("Binks")
+ expect(tokens[1][0]).to eq("rm")
+ expect(tokens[1][1]).to eq("/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'")
+ expect(tokens.size).to eq(1)
+ expect(tokens[0].size).to eq(3)
+ expect(tokens[0][0]).to eq("set")
+ expect(tokens[0][1]).to eq("/foo/JarJar")
+ expect(tokens[0][2]).to eq("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"')
+ expect(tokens.size).to eq(1)
+ expect(tokens[0].size).to eq(3)
+ expect(tokens[0][0]).to eq("set")
+ expect(tokens[0][1]).to eq('/JarJar')
+ expect(tokens[0][2]).to eq('Binks is my copilot')
+ end
+
+ it "should accept mixed ticks" do
+ @resource[:context] = "/foo/"
+ tokens = @provider.parse_commands('set JarJar "Some \'Test\'"')
+ expect(tokens.size).to eq(1)
+ expect(tokens[0].size).to eq(3)
+ expect(tokens[0][0]).to eq("set")
+ expect(tokens[0][1]).to eq('/foo/JarJar')
+ expect(tokens[0][2]).to eq("Some \'Test\'")
+ end
+
+ it "should handle predicates with literals" do
+ @resource[:context] = "/foo/"
+ tokens = @provider.parse_commands("rm */*[module='pam_console.so']")
+ expect(tokens).to eq([["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' ]")
+ expect(tokens).to eq([["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']")
+ expect(tokens).to eq([["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(" "))
+ expect(tokens).to eq([ args ])
+ end
+
+ it "should handle escaped doublequotes in doublequoted string" do
+ @resource[:context] = "/foo/"
+ tokens = @provider.parse_commands("set /foo \"''\\\"''\"")
+ expect(tokens).to eq([[ "set", "/foo", "''\"''" ]])
+ end
+
+ it "should preserve escaped single quotes in double quoted strings" do
+ @resource[:context] = "/foo/"
+ tokens = @provider.parse_commands("set /foo \"\\'\"")
+ expect(tokens).to eq([[ "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 "))
+ expect(tokens).to eq([ 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 "))
+ expect(tokens).to eq([[ "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 "))
+ expect(tokens).to eq([[ "set", "/white\\ space/key", "value" ]])
+ end
+
+ it "should remove trailing slashes" do
+ @resource[:context] = "/foo/"
+ tokens = @provider.parse_commands("set foo/ bar")
+ expect(tokens).to eq([[ "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"]
+ expect(@provider.process_get(command)).to eq(true)
+ end
+
+ it "should return true for a != match" do
+ command = ["get", "fake value", "!=", "value"]
+ expect(@provider.process_get(command)).to eq(false)
+ end
+
+ it "should return true for a =~ match" do
+ command = ["get", "fake value", "=~", "val*"]
+ expect(@provider.process_get(command)).to eq(true)
+ end
+
+ it "should return false for a == nonmatch" do
+ command = ["get", "fake value", "=~", "num*"]
+ expect(@provider.process_get(command)).to eq(false)
+ end
+ end
+
+ describe "values filters" do
+ before do
+ augeas = stub("augeas", :match => ["set", "of", "values"])
+ augeas.stubs(:get).returns('set').then.returns('of').then.returns('values')
+ augeas.stubs("close")
+ @provider = provider_class.new(@resource)
+ @provider.aug = augeas
+ end
+
+ it "should return true for includes match" do
+ command = ["values", "fake value", "include values"]
+ expect(@provider.process_values(command)).to eq(true)
+ end
+
+ it "should return false for includes non match" do
+ command = ["values", "fake value", "include JarJar"]
+ expect(@provider.process_values(command)).to eq(false)
+ end
+
+ it "should return true for includes match" do
+ command = ["values", "fake value", "not_include JarJar"]
+ expect(@provider.process_values(command)).to eq(true)
+ end
+
+ it "should return false for includes non match" do
+ command = ["values", "fake value", "not_include values"]
+ expect(@provider.process_values(command)).to eq(false)
+ end
+
+ it "should return true for an array match" do
+ command = ["values", "fake value", "== ['set', 'of', 'values']"]
+ expect(@provider.process_values(command)).to eq(true)
+ end
+
+ it "should return false for an array non match" do
+ command = ["values", "fake value", "== ['this', 'should', 'not', 'match']"]
+ expect(@provider.process_values(command)).to eq(false)
+ end
+
+ it "should return false for an array match with noteq" do
+ command = ["values", "fake value", "!= ['set', 'of', 'values']"]
+ expect(@provider.process_values(command)).to eq(false)
+ end
+
+ it "should return true for an array non match with noteq" do
+ command = ["values", "fake value", "!= ['this', 'should', 'not', 'match']"]
+ expect(@provider.process_values(command)).to eq(true)
+ 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"]
+ expect(@provider.process_match(command)).to eq(true)
+ end
+
+ it "should return false for a size non match" do
+ command = ["match", "fake value", "size < 3"]
+ expect(@provider.process_match(command)).to eq(false)
+ end
+
+ it "should return true for includes match" do
+ command = ["match", "fake value", "include values"]
+ expect(@provider.process_match(command)).to eq(true)
+ end
+
+ it "should return false for includes non match" do
+ command = ["match", "fake value", "include JarJar"]
+ expect(@provider.process_match(command)).to eq(false)
+ end
+
+ it "should return true for includes match" do
+ command = ["match", "fake value", "not_include JarJar"]
+ expect(@provider.process_match(command)).to eq(true)
+ end
+
+ it "should return false for includes non match" do
+ command = ["match", "fake value", "not_include values"]
+ expect(@provider.process_match(command)).to eq(false)
+ end
+
+ it "should return true for an array match" do
+ command = ["match", "fake value", "== ['set', 'of', 'values']"]
+ expect(@provider.process_match(command)).to eq(true)
+ end
+
+ it "should return false for an array non match" do
+ command = ["match", "fake value", "== ['this', 'should', 'not', 'match']"]
+ expect(@provider.process_match(command)).to eq(false)
+ end
+
+ it "should return false for an array match with noteq" do
+ command = ["match", "fake value", "!= ['set', 'of', 'values']"]
+ expect(@provider.process_match(command)).to eq(false)
+ end
+
+ it "should return true for an array non match with noteq" do
+ command = ["match", "fake value", "!= ['this', 'should', 'not', 'match']"]
+ expect(@provider.process_match(command)).to eq(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"])
+ expect(@provider.need_to_run?).to eq(true)
+ end
+
+ it "should return true when a get filter matches" do
+ @resource[:onlyif] = "get path == value"
+ @augeas.stubs("get").returns("value")
+ expect(@provider.need_to_run?).to eq(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")
+ expect(@provider.need_to_run?).to eq(true)
+ end
+
+ it "should return false when a get string compare is false" do
+ @resource[:onlyif] = "get a19path > a2"
+ @augeas.stubs("get").returns("a19")
+ expect(@provider.need_to_run?).to eq(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")
+ expect(@provider.need_to_run?).to eq(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")
+ expect(@provider.need_to_run?).to eq(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")
+ expect(@provider.need_to_run?).to eq(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")
+ expect(@provider.need_to_run?).to eq(false)
+ end
+ end
+ describe "performing is_numeric checks (#22617)" do
+ it "should return false for nil" do
+ expect(@provider.is_numeric?(nil)).to eq(false)
+ end
+ it "should return true for Integers" do
+ expect(@provider.is_numeric?(9)).to eq(true)
+ end
+ it "should return true for numbers in Strings" do
+ expect(@provider.is_numeric?('9')).to eq(true)
+ end
+ it "should return false for non-number Strings" do
+ expect(@provider.is_numeric?('x9')).to eq(false)
+ end
+ it "should return false for other types" do
+ expect(@provider.is_numeric?([true])).to eq(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")
+ expect(@provider.need_to_run?).to eq(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"])
+ expect(@provider.need_to_run?).to eq(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"])
+ expect(@provider.need_to_run?).to eq(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"])
+ expect(@provider.need_to_run?).to eq(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"])
+ expect(@provider.need_to_run?).to eq(true)
+ end
+
+ #Ticket 5211 testing
+ it "should return false when a size does equal the provided value" do
+ @resource[:onlyif] = "match path size != 3"
+ @augeas.stubs("match").returns(["set", "of", "values"])
+ expect(@provider.need_to_run?).to eq(false)
+ end
+
+ [true, false].product([true, false]) do |cfg, param|
+ describe "and Puppet[:show_diff] is #{cfg} and show_diff => #{param}" do
+ let(:file) { "/some/random/file" }
+
+ before(:each) do
+ Puppet[:show_diff] = cfg
+ @resource[:show_diff] = param
+
+ @resource[:root] = ""
+ @resource[:context] = "/files"
+ @resource[:changes] = ["set #{file}/foo bar"]
+
+ File.stubs(:delete)
+ @provider.stubs(:get_augeas_version).returns("0.10.0")
+ @provider.stubs("diff").with("#{file}", "#{file}.augnew").returns("diff")
+
+ @augeas.stubs(:set).returns(true)
+ @augeas.stubs(:save).returns(true)
+ @augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved"])
+ @augeas.stubs(:get).with("/augeas/events/saved").returns("/files#{file}")
+ @augeas.stubs(:set).with("/augeas/save", "newfile")
+ end
+
+ if cfg && param
+ it "should display a diff" do
+ expect(@provider).to be_need_to_run
+
+ expect(@logs[0].message).to eq("\ndiff")
+ end
+ else
+ it "should not display a diff" do
+ expect(@provider).to be_need_to_run
+
+ expect(@logs).to be_empty
+ end
+ end
+ end
+ end
+
+ # Ticket 2728 (diff files)
+ describe "and configured to show diffs" do
+ before(:each) do
+ Puppet[:show_diff] = true
+ @resource[: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")
+
+ expect(@provider).to be_need_to_run
+
+ expect(@logs[0].message).to eq("\ndiff")
+ 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}")
+
+ expect(@provider).to 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")
+
+ expect(@provider).to 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()
+ expect(@provider).not_to 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("")
+ expect(@provider).to 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()
+ expect(@provider).to 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)
+ expect { @provider.need_to_run? }.to 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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(:executed)
+ end
+
+ describe "touch command" do
+ it "should clear missing path" do
+ @resource[:changes] = "touch Jar/Jar"
+ @resource[:context] = "/foo/"
+ @augeas.expects(:match).with("/foo/Jar/Jar").returns([])
+ @augeas.expects(:clear).with("/foo/Jar/Jar").returns(true)
+ @augeas.expects(:save).returns(true)
+ @augeas.expects(:close)
+ expect(@provider.execute_changes).to eq(:executed)
+ end
+
+ it "should not change on existing path" do
+ @resource[:changes] = "touch Jar/Jar"
+ @resource[:context] = "/foo/"
+ @augeas.expects(:match).with("/foo/Jar/Jar").returns(["/foo/Jar/Jar"])
+ @augeas.expects(:clear).never
+ @augeas.expects(:save).returns(true)
+ @augeas.expects(:close)
+ expect(@provider.execute_changes).to eq(:executed)
+ end
+ 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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(:executed)
+ end
+
+ it "should handle rename commands" do
+ @resource[:changes] = "rename Jar/Jar Binks"
+ @resource[:context] = "/foo/"
+ @augeas.expects(:rename).with("/foo/Jar/Jar", "Binks").returns(true)
+ @augeas.expects(:save).returns(true)
+ @augeas.expects(:close)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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)
+ expect(@provider.execute_changes).to eq(: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
+ it "should not clobber the file if it's a symlink" do
+ Puppet::Util::Storage.stubs(:store)
+
+ link = Tempfile.new('link')
+ target = Tempfile.new('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
+
+ expect(File.ftype(link)).to eq('link')
+ expect(Puppet::FileSystem.readlink(link)).to eq(target)
+ expect(File.read(target)).to match(/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
+ expect(aug).not_to eq(nil)
+ expect(aug.match("/files/etc/fstab")).to eq(["/files/etc/fstab"])
+ expect(aug.match("/files/etc/hosts")).to eq(["/files/etc/hosts"])
+ expect(aug.match("/files/etc/test")).to eq([])
+ end
+
+ it "should report load errors to debug only" do
+ @provider.expects(:print_load_errors).with(nil)
+ aug = @provider.open_augeas
+ expect(aug).not_to eq(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
+ expect(aug).not_to eq(nil)
+ expect(aug.match("/files/etc/fstab")).to eq([])
+ expect(aug.match("/files/etc/hosts")).to eq(["/files/etc/hosts"])
+ expect(aug.match("/files/etc/test")).to eq([])
+ end
+
+ it "should also load lenses from load_path" do
+ @resource[:load_path] = my_fixture_dir
+
+ aug = @provider.open_augeas
+ expect(aug).not_to eq(nil)
+ expect(aug.match("/files/etc/fstab")).to eq(["/files/etc/fstab"])
+ expect(aug.match("/files/etc/hosts")).to eq(["/files/etc/hosts"])
+ expect(aug.match("/files/etc/test")).to eq(["/files/etc/test"])
+ end
+
+ it "should also load lenses from pluginsync'd path" do
+ Puppet[:libdir] = my_fixture_dir
+
+ aug = @provider.open_augeas
+ expect(aug).not_to eq(nil)
+ expect(aug.match("/files/etc/fstab")).to eq(["/files/etc/fstab"])
+ expect(aug.match("/files/etc/hosts")).to eq(["/files/etc/hosts"])
+ expect(aug.match("/files/etc/test")).to eq(["/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
+ expect(aug).not_to eq(nil)
+ expect(aug.match("/files/etc/fstab")).to eq(["/files/etc/fstab"])
+ expect(aug.match("/files/etc/hosts")).to eq([])
+ 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
+ expect(aug).not_to eq(nil)
+ expect(aug.match("/files/etc/fstab")).to eq([])
+ expect(aug.match("/files/etc/hosts")).to eq([])
+ expect(aug.match("/files/etc/test")).to eq(["/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
+ expect(aug).not_to eq(nil)
+ expect(aug.match("/files/etc/fstab")).to eq(["/files/etc/fstab"])
+ expect(aug.match("/files/etc/hosts")).to eq(["/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
+ expect(aug).not_to eq(nil)
+ expect(aug.match("/files/etc/fstab")).to eq(["/files/etc/fstab"])
+ expect(aug.match("/files/etc/hosts")).to eq(["/files/etc/hosts"])
+ end
+ end
+ end
+
+ describe "get_load_path" do
+ it "should offer no load_path by default" do
+ expect(@provider.get_load_path(@resource)).to eq("")
+ end
+
+ it "should offer one path from load_path" do
+ @resource[:load_path] = "/foo"
+ expect(@provider.get_load_path(@resource)).to eq("/foo")
+ end
+
+ it "should offer multiple colon-separated paths from load_path" do
+ @resource[:load_path] = "/foo:/bar:/baz"
+ expect(@provider.get_load_path(@resource)).to eq("/foo:/bar:/baz")
+ end
+
+ it "should offer multiple paths in array from load_path" do
+ @resource[:load_path] = ["/foo", "/bar", "/baz"]
+ expect(@provider.get_load_path(@resource)).to eq("/foo:/bar:/baz")
+ end
+
+ it "should offer pluginsync augeas/lenses subdir" do
+ Puppet[:libdir] = my_fixture_dir
+ expect(@provider.get_load_path(@resource)).to eq("#{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"]
+ expect(@provider.get_load_path(@resource)).to eq("/foo:/bar:/baz:#{my_fixture_dir}/augeas/lenses")
+ end
+ end
+end
diff --git a/spec/unit/type/augeas_spec.rb b/spec/unit/type/augeas_spec.rb
new file mode 100644
index 0000000..a1f2f0f
--- /dev/null
+++ b/spec/unit/type/augeas_spec.rb
@@ -0,0 +1,119 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+augeas = Puppet::Type.type(:augeas)
+
+describe augeas do
+ describe "when augeas is present", :if => Puppet.features.augeas? do
+ it "should have a default provider inheriting from Puppet::Provider" do
+ expect(augeas.defaultprovider.ancestors).to be_include(Puppet::Provider)
+ end
+
+ it "should have a valid provider" do
+ expect(augeas.new(:name => "foo").provider.class.ancestors).to be_include(Puppet::Provider)
+ end
+ end
+
+ describe "basic structure" do
+ it "should be able to create an instance" do
+ provider_class = Puppet::Type::Augeas.provider(Puppet::Type::Augeas.providers[0])
+ Puppet::Type::Augeas.expects(:defaultprovider).returns provider_class
+ expect(augeas.new(:name => "bar")).not_to be_nil
+ end
+
+ it "should have a parse_commands feature" do
+ expect(augeas.provider_feature(:parse_commands)).not_to be_nil
+ end
+
+ it "should have a need_to_run? feature" do
+ expect(augeas.provider_feature(:need_to_run?)).not_to be_nil
+ end
+
+ it "should have an execute_changes feature" do
+ expect(augeas.provider_feature(:execute_changes)).not_to be_nil
+ end
+
+ properties = [:returns]
+ params = [:name, :context, :onlyif, :changes, :root, :load_path, :type_check, :show_diff]
+
+ properties.each do |property|
+ it "should have a #{property} property" do
+ expect(augeas.attrclass(property).ancestors).to be_include(Puppet::Property)
+ end
+
+ it "should have documentation for its #{property} property" do
+ expect(augeas.attrclass(property).doc).to be_instance_of(String)
+ end
+ end
+
+ params.each do |param|
+ it "should have a #{param} parameter" do
+ expect(augeas.attrclass(param).ancestors).to be_include(Puppet::Parameter)
+ end
+
+ it "should have documentation for its #{param} parameter" do
+ expect(augeas.attrclass(param).doc).to be_instance_of(String)
+ end
+ end
+ end
+
+ describe "default values" do
+ before do
+ provider_class = augeas.provider(augeas.providers[0])
+ augeas.expects(:defaultprovider).returns provider_class
+ end
+
+ it "should be blank for context" do
+ expect(augeas.new(:name => :context)[:context]).to eq("")
+ end
+
+ it "should be blank for onlyif" do
+ expect(augeas.new(:name => :onlyif)[:onlyif]).to eq("")
+ end
+
+ it "should be blank for load_path" do
+ expect(augeas.new(:name => :load_path)[:load_path]).to eq("")
+ end
+
+ it "should be / for root" do
+ expect(augeas.new(:name => :root)[:root]).to eq("/")
+ end
+
+ it "should be false for type_check" do
+ expect(augeas.new(:name => :type_check)[:type_check]).to eq(:false)
+ end
+ end
+
+ describe "provider interaction" do
+
+ it "should return 0 if it does not need to run" do
+ provider = stub("provider", :need_to_run? => false)
+ resource = stub('resource', :resource => nil, :provider => provider, :line => nil, :file => nil)
+ changes = augeas.attrclass(:returns).new(:resource => resource)
+ expect(changes.retrieve).to eq(0)
+ end
+
+ it "should return :need_to_run if it needs to run" do
+ provider = stub("provider", :need_to_run? => true)
+ resource = stub('resource', :resource => nil, :provider => provider, :line => nil, :file => nil)
+ changes = augeas.attrclass(:returns).new(:resource => resource)
+ expect(changes.retrieve).to eq(:need_to_run)
+ end
+ end
+
+ describe "loading specific files" do
+ it "should require lens when incl is used" do
+ expect { augeas.new(:name => :no_lens, :incl => "/etc/hosts")}.to raise_error(Puppet::Error)
+ end
+
+ it "should require incl when lens is used" do
+ expect { augeas.new(:name => :no_incl, :lens => "Hosts.lns") }.to raise_error(Puppet::Error)
+ end
+
+ it "should set the context when a specific file is used" do
+ fake_provider = stub_everything "fake_provider"
+ augeas.stubs(:defaultprovider).returns fake_provider
+ expect(augeas.new(:name => :no_incl, :lens => "Hosts.lns", :incl => "/etc/hosts")[:context]).to eq("/files/etc/hosts")
+ end
+ end
+end