#!/usr/bin/python3 # Copyright (C) 2017 import collections.abc import logging import os # requires posix from augeas import Augeas AUGEAS_LOAD_PATH = '/augeas/load/' AUGEAS_FILES_PATH = '/files/' AUGEAS_ERROR_PATH = '//error' log = logging.getLogger('augeas') def join(*paths): """ join two Augeas tree paths FIXME: Beware: // is normalized to / """ norm_paths = [os.path.normpath(path) for path in paths] # first path must be absolute assert norm_paths[0][0] == '/' new_paths = [norm_paths[0]] # relativize all other paths so join works as expected for path in norm_paths[1:]: if path.startswith('/'): path = path[1:] new_paths.append(path) new_path = os.path.join(*new_paths) log.debug("join: new_path %s", new_path) return os.path.normpath(new_path) class AugeasWrapper: """python-augeas higher-level wrapper. Load single augeas lens and configuration file. Exposes configuration file as AugeasNode object with dict-like interface. AugeasWrapper can be used in with statement in the same way as file does. """ def __init__(self, confpath, lens, root=None, loadpath=None, flags=Augeas.NO_MODL_AUTOLOAD | Augeas.NO_LOAD | Augeas.ENABLE_SPAN): """Parse configuration file using given lens. Params: confpath (str): Absolute path to the configuration file lens (str): Name of module containing Augeas lens root: passed down to original Augeas flags: passed down to original Augeas loadpath: passed down to original Augeas flags: passed down to original Augeas """ log.debug('loadpath: %s', loadpath) log.debug('confpath: %s', confpath) self._aug = Augeas(root=root, loadpath=loadpath, flags=flags) # /augeas/load/{lens} aug_load_path = join(AUGEAS_LOAD_PATH, lens) # /augeas/load/{lens}/lens = {lens}.lns self._aug.set(join(aug_load_path, 'lens'), '%s.lns' % lens) # /augeas/load/{lens}/incl[0] = {confpath} self._aug.set(join(aug_load_path, 'incl[0]'), confpath) self._aug.load() errors = self._aug.match(AUGEAS_ERROR_PATH) if errors: err_msg = '\n'.join( ["{}: {}".format(e, self._aug.get(e)) for e in errors] ) raise RuntimeError(err_msg) path = join(AUGEAS_FILES_PATH, confpath) paths = self._aug.match(path) if len(paths) != 1: raise ValueError('path %s did not match exactly once' % path) self.tree = AugeasNode(self._aug, path) self._loaded = True def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.save() self.close() def save(self): """Save Augeas tree to its original file.""" assert self._loaded try: self._aug.save() except IOError as exc: log.exception(exc) for err_path in self._aug.match('//error'): log.error('%s: %s', err_path, self._aug.get(os.path.join(err_path, 'message'))) raise def close(self): """ close Augeas library After calling close() the object must not be used anymore. """ assert self._loaded self._aug.close() del self._aug self._loaded = False def match(self, path): """Yield AugeasNodes matching given expression.""" assert self._loaded assert path log.debug('tree match %s', path) for matched_path in self._aug.match(path): yield AugeasNode(self._aug, matched_path) class AugeasNode(collections.abc.MutableMapping): """One Augeas tree node with dict-like interface.""" def __init__(self, aug, path): """ Args: aug (AugeasWrapper or Augeas): Augeas library instance path (str): absolute path in Augeas tree matching single node BEWARE: There are no sanity checks of given path for performance reasons. """ assert aug assert path assert path.startswith('/') self._aug = aug self._path = path self._span = None @property def path(self): """canonical path in Augeas tree, read-only""" return self._path @property def value(self): """ get value of this node in Augeas tree """ value = self._aug.get(self._path) log.debug('tree get: %s = %s', self._path, value) return value @value.setter def value(self, value): """ set value of this node in Augeas tree """ log.debug('tree set: %s = %s', self._path, value) self._aug.set(self._path, value) @property def span(self): if self._span is None: self._span = "char position %s" % self._aug.span(self._path)[5] return self._span @property def char(self): return self._aug.span(self._path)[5] def __len__(self): """ number of items matching this path It is always 1 after __init__() but it may change as Augeas tree changes. """ return len(self._aug.match(self._path)) def __getitem__(self, key): if isinstance(key, int): # int is a shortcut to write [int] target_path = '%s[%s]' % (self._path, key) else: target_path = self._path + key log.debug('tree getitem: target_path %s', target_path) paths = self._aug.match(target_path) if len(paths) != 1: raise KeyError('path %s did not match exactly once' % target_path) return AugeasNode(self._aug, target_path) def __delitem__(self, key): log.debug('tree delitem: %s + %s', self._path, key) target_path = self._path + key log.debug('tree delitem: target_path %s', target_path) self._aug.remove(target_path) def __setitem__(self, key, value): assert isinstance(value, AugeasNode) target_path = self.path + key self._aug.copy(value.path, target_path) def __iter__(self): self_path_len = len(self._path) assert self_path_len > 0 log.debug('tree iter: %s', self._path) for new_path in self._aug.match(self._path): if len(new_path) == self_path_len: yield '' else: yield new_path[self_path_len - 1:] def match(self, subpath): """Yield AugeasNodes matching given sub-expression.""" assert subpath.startswith("/") match_path = "%s%s" % (self._path, subpath) log.debug('tree match %s: %s', match_path, self._path) for matched_path in self._aug.match(match_path): yield AugeasNode(self._aug, matched_path) def __repr__(self): return 'AugeasNode(%s)' % self._path