diff options
-rw-r--r-- | lib/puppet/feature/augeas.rb | 3 | ||||
-rw-r--r-- | lib/puppet/provider/augeas/augeas.rb | 573 | ||||
-rw-r--r-- | lib/puppet/type/augeas.rb | 211 | ||||
-rw-r--r-- | spec/acceptance/hosts.rb | 78 | ||||
-rw-r--r-- | spec/acceptance/puppet.rb | 46 | ||||
-rw-r--r-- | spec/acceptance/services.rb | 73 | ||||
-rw-r--r-- | spec/fixtures/unit/provider/augeas/augeas/augeas/lenses/test.aug | 13 | ||||
-rw-r--r-- | spec/fixtures/unit/provider/augeas/augeas/etc/fstab | 10 | ||||
-rw-r--r-- | spec/fixtures/unit/provider/augeas/augeas/etc/hosts | 6 | ||||
-rw-r--r-- | spec/fixtures/unit/provider/augeas/augeas/etc/test | 3 | ||||
-rw-r--r-- | spec/fixtures/unit/provider/augeas/augeas/test.aug | 13 | ||||
-rw-r--r-- | spec/unit/provider/augeas/augeas_spec.rb | 1033 | ||||
-rw-r--r-- | spec/unit/type/augeas_spec.rb | 119 |
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 |