aboutsummaryrefslogtreecommitdiff
path: root/lib/puppet/provider
diff options
context:
space:
mode:
Diffstat (limited to 'lib/puppet/provider')
-rw-r--r--lib/puppet/provider/augeas/augeas.rb573
1 files changed, 573 insertions, 0 deletions
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