class CFA::AugeasWriter::LazyOperations

Represents an operation that needs to be done after all modifications.

The reason to have this class is that Augeas renumbers its arrays after some operations like `rm` or `insert` so previous paths are no longer valid. For this reason these sensitive operations that change paths need to be done at the end and with careful order. See www.redhat.com/archives/augeas-devel/2017-March/msg00002.html

@note This class depends on ordered operations. So adding and removing entries has to be done in order how they are placed in tree.

Attributes

aug[R]

Public Class Methods

new(aug) click to toggle source

@param aug result of Augeas.create

# File lib/cfa/augeas_parser/writer.rb, line 171
def initialize(aug)
  @aug = aug
  @operations = []
end

Public Instance Methods

add(located_entry) click to toggle source
# File lib/cfa/augeas_parser/writer.rb, line 176
def add(located_entry)
  @operations << { type: :add, located_entry: located_entry }
end
remove(located_entry) click to toggle source
# File lib/cfa/augeas_parser/writer.rb, line 180
def remove(located_entry)
  @operations << { type: :remove, path: located_entry.path }
end
run() click to toggle source

starts all previously inserted operations

# File lib/cfa/augeas_parser/writer.rb, line 185
def run
  # the reverse order is needed because if there are two operations
  # one after another then the latter cannot affect the former
  @operations.reverse_each do |operation|
    case operation[:type]
    when :remove then remove_entry(operation[:path])
    when :add
      located_entry = operation[:located_entry]
      add_entry(located_entry)
    else
      raise "Invalid lazy operation #{operation.inspect}"
    end
  end
end

Private Instance Methods

add_entry(located_entry) click to toggle source

Adds entry to tree. At first it finds where to add it to be in correct place and then sets its value. Recursive if needed. In recursive case it is already known that whole sub-tree is also new and just added.

# File lib/cfa/augeas_parser/writer.rb, line 229
def add_entry(located_entry)
  path = insert_entry(located_entry)
  set_new_value(path, located_entry)
end
add_subtree(tree, prefix) click to toggle source

Adds new subtree. Simplified version of common write as it is known that all entries will be just added. @param tree [CFA::AugeasTree] to add @param prefix [String] prefix where to place tree

# File lib/cfa/augeas_parser/writer.rb, line 256
def add_subtree(tree, prefix)
  tree.all_data.each do |entry|
    located_entry = LocatedEntry.new(tree, entry, prefix)
    # universal path that handles also new elements for arrays
    path = "#{prefix}/#{located_entry.key}[last()+1]"
    set_new_value(path, located_entry)
  end
end
find_preceding_index(paths, preceding) click to toggle source
# File lib/cfa/augeas_parser/writer.rb, line 315
def find_preceding_index(paths, preceding)
  path = preceding.path
  # common case, just included
  return paths.index(path) if paths.include?(path)

  # not found, so it means that some collection or single entry switch
  new_path = +"/"
  path.split("/").each do |element|
    new_path << "/" unless new_path.end_with?("/")
    new_path << pick_candidate(paths, new_path, element)
  end

  paths.index(new_path) ||
    raise("Cannot find path #{preceding.path} in #{paths.inspect}")
end
insert_after(preceding, located_entry) click to toggle source

Insert key after preceding. @see insert_entry @param preceding [LocatedEntry] entry after which the new one goes @param located_entry [LocatedEntry] entry to insert @return [String] where value should be written.

# File lib/cfa/augeas_parser/writer.rb, line 295
def insert_after(preceding, located_entry)
  res = aug.insert(preceding.path, located_entry.key, false)
  # if insert failed it means, that previous preceding entry was single
  # element and now it is multiple ones, so try to first element as add
  # is done in reverse order
  if res == -1
    # TODO: what about deep nesting of trees where upper level change
    # from single to collection?
    aug.insert(preceding.path + "[1]", located_entry.key, false)
  end
  path_after(preceding)
end
insert_entry(located_entry) click to toggle source

It inserts a key at given position without setting its value. Its logic is to set it after the last valid entry. If it is not defined then tries to place it before the first valid entry in tree. If there is no entry in tree, then does not insert a position, which means that subsequent setting of value appends it to the end.

@param located_entry [LocatedEntry] entry to insert @return [String] where value should be written. Can

contain path expressions.
See https://github.com/hercules-team/augeas/wiki/Path-expressions
# File lib/cfa/augeas_parser/writer.rb, line 275
def insert_entry(located_entry)
  # entries with add not exist yet
  preceding = located_entry.preceding_existing
  prefix = located_entry.prefix
  if preceding
    insert_after(preceding, located_entry)
  # entries with remove is already removed, otherwise find previously
  elsif located_entry.any_following?
    aug.insert(prefix + "/*[1]", located_entry.key, true)
    aug.match(prefix + "/*[1]").first
  else
    "#{prefix}/#{located_entry.key}"
  end
end
path_after(preceding) click to toggle source

Finds path immediately after preceding entry @param preceding [LocatedEntry]

# File lib/cfa/augeas_parser/writer.rb, line 310
def path_after(preceding)
  paths = aug.match(preceding.prefix + "/*")
  paths[find_preceding_index(paths, preceding) + 1]
end
path_to_remove(path) click to toggle source

Finds path to remove, as path can be meanwhile renumbered, see remove_entry

# File lib/cfa/augeas_parser/writer.rb, line 216
def path_to_remove(path)
  if aug.match(path).size == 1
    path
  elsif !aug.match(path + "[1]").empty?
    path + "[1]"
  else
    raise "Unknown augeas path #{path}"
  end
end
pick_candidate(paths, new_path, element) click to toggle source

it returns variant of element that exists in path

# File lib/cfa/augeas_parser/writer.rb, line 332
def pick_candidate(paths, new_path, element)
  # NOTE: order here is important due to early matching
  candidates = [element + "/", element + "[1]",
                element.sub(/\[\d+\]/, ""), element]
  paths.each do |p|
    candidates.each do |c|
      return c if p.start_with?(new_path + c)
    end
  end
end
remove_entry(path) click to toggle source

Removes entry from tree. If path does not exist, then tries if it has changed to a collection: If we remove and re-add a single key then because of the laziness Augeas will first see the addition, making a 2 member collection, so we need to remove “key” instead of “key”. @param path [String] original path name to remove

# File lib/cfa/augeas_parser/writer.rb, line 210
def remove_entry(path)
  aug.rm(path_to_remove(path))
end
set_new_value(path, located_entry) click to toggle source

Sets new value to given path. It is used for values that are not yet in Augeas tree. If needed it does recursive adding. @param path [String] path which can contain Augeas path expression for

key of new value

@param located_entry [LocatedEntry] entry to write @see github.com/hercules-team/augeas/wiki/Path-expressions

# File lib/cfa/augeas_parser/writer.rb, line 240
def set_new_value(path, located_entry)
  aug.set(path, located_entry.entry_value)
  # we need to get new path as path used in aug.set can contains
  # "[last() + 1]", so adding subtree to it, adds additional entry.
  # So here, we replace "[last() + 1]" with "[last()]" so it will match
  # path created by previous aug.set
  match_str = path.gsub(/\[\s*last\(\)\s*\+\s*1\]/, "[last()]")

  new_path = aug.match(match_str).first
  add_subtree(located_entry.entry_tree, new_path)
end