Example #1
0
File: cli.py Project: jkrysl/fmf
    def __init__(self, arguments=None, path=None):
        """ Prepare the parser. """
        # Change current working directory (used for testing)
        if path is not None:
            os.chdir(path)
        # Split command line if given as a string (used for testing)
        if isinstance(arguments, type("")): # pragma: no cover
            try:
                # This is necessary for Python 2.6
                self.arguments = [arg.decode('utf8')
                    for arg in shlex.split(arguments.encode('utf8'))]
            except AttributeError:
                self.arguments = shlex.split(arguments)
        # Otherwise use sys.argv (plus decode unicode for Python 2)
        if arguments is None: # pragma: no cover
            try:
                self.arguments = [arg.decode("utf-8") for arg in sys.argv]
            except AttributeError:
                self.arguments = sys.argv
        # Enable debugging output if requested
        if "--debug" in self.arguments:
            utils.log.setLevel(utils.LOG_DEBUG)

        # Handle subcommands (mapped to format_* methods)
        self.parser = argparse.ArgumentParser(
            usage="fmf command [options]\n" + __doc__)
        self.parser.add_argument('command', help='Command to run')
        self.command = self.parser.parse_args(self.arguments[1:2]).command
        if not hasattr(self, "command_" + self.command):
            self.parser.print_help()
            raise utils.GeneralError(
                "Unrecognized command: '{0}'".format(self.command))
        # Initialize the rest and run the subcommand
        self.output = ""
        getattr(self, "command_" + self.command)()
Example #2
0
File: cli.py Project: thrix/fmf
    def __init__(self, arguments=None, path=None):
        """ Prepare the parser. """
        # Change current working directory (used for testing)
        if path is not None:
            os.chdir(path)
        # Split command line if given as a string (used for testing)
        if isinstance(arguments, str):
            self.arguments = shlex.split(arguments)
        # Otherwise use sys.argv
        if arguments is None:
            self.arguments = sys.argv
        # Enable debugging output if requested
        if "--debug" in self.arguments:
            utils.log.setLevel(utils.LOG_DEBUG)

        # Handle subcommands (mapped to format_* methods)
        self.parser = argparse.ArgumentParser(usage="fmf command [options]\n" +
                                              __doc__)
        self.parser.add_argument('command', help='Command to run')
        self.command = self.parser.parse_args(self.arguments[1:2]).command
        if not hasattr(self, "command_" + self.command):
            self.parser.print_help()
            raise utils.GeneralError("Unrecognized command: '{0}'".format(
                self.command))
        # Initialize the rest and run the subcommand
        self.output = ""
        getattr(self, "command_" + self.command)()
Example #3
0
    def __init__(self, data, name=None, parent=None):
        """
        Initialize metadata tree from directory path or data dictionary

        Data parameter can be either a string with directory path to be
        explored or a dictionary with the values already prepared.
        """

        # Bail out if no data and no parent given
        if not data and not parent:
            raise utils.GeneralError(
                "No data or parent provided to initialize the tree.")

        # Initialize family relations, object data and source files
        self.parent = parent
        self.children = dict()
        self.data = dict()
        self.sources = list()
        self.root = None
        self.version = utils.VERSION
        self.original_data = dict()
        self._commit = None
        self._raw_data = dict()
        # Track whether the data dictionary has been updated
        # (needed to prevent removing nodes with an empty dict).
        self._updated = False

        # Store symlinks in while walking tree in grow() to detect
        # symlink loops
        if parent is None:
            self._symlinkdirs = []
        else:
            self._symlinkdirs = parent._symlinkdirs

        # Special handling for top parent
        if self.parent is None:
            self.name = "/"
            if not isinstance(data, dict):
                self._initialize(path=data)
                data = self.root
        # Handle child node creation
        else:
            self.root = self.parent.root
            self.name = os.path.join(self.parent.name, name)

        # Update data from a dictionary (handle empty nodes)
        if isinstance(data, dict) or data is None:
            self.update(data)
        # Grow the tree from a directory path
        else:
            self.grow(data)

        # Apply inheritance when all scattered data are gathered.
        # This is done only once, from the top parent object.
        if self.parent is None:
            self.inherit()

        log.debug("New tree '{0}' created.".format(self))
Example #4
0
    def node(reference):
        """
        Return Tree node referenced by the fmf identifier

        Keys supported in the reference:

        url .... git repository url (optional)
        ref .... branch, tag or commit (default branch if not provided)
        path ... metadata tree root ('.' by default)
        name ... tree node name ('/' by default)

        See the documentation for the full fmf id specification:
        https://fmf.readthedocs.io/en/latest/concept.html#identifiers
        Raises ReferenceError if referenced node does not exist.
        """

        # Fetch remote git repository
        if 'url' in reference:
            path = reference.get('path', '.').lstrip('/')
            # Create lock path to fetch/read git from URL to the cache
            cache_dir = utils.get_cache_directory()
            # Use .read.lock suffix (different from the inner fetch lock)
            lock_path = os.path.join(
                cache_dir, reference["url"].replace('/', '_')) + '.read.lock'
            try:
                with FileLock(lock_path, timeout=NODE_LOCK_TIMEOUT) as lock:
                    # Write PID to lockfile so we know which process got it
                    with open(lock.lock_file, 'w') as lock_file:
                        lock_file.write(str(os.getpid()))
                    repository = utils.fetch(
                        reference.get('url'), reference.get('ref'))
                    root = os.path.join(repository, path)
                    tree = Tree(root)
            except Timeout:
                raise utils.GeneralError(
                    "Failed to acquire lock for {0} within {1} seconds".format(
                    lock_path, NODE_LOCK_TIMEOUT))
        # Use local files
        else:
            root = reference.get('path', '.')
            if not root.startswith('/') and root != '.':
                raise utils.ReferenceError(
                    'Relative path "%s" specified.' % root)
            tree = Tree(root)
        found_node = tree.find(reference.get('name', '/'))
        if found_node is None:
            raise utils.ReferenceError(
                "No tree node found for '{0}' reference".format(reference))
        # FIXME Should be able to remove .cache if required
        return found_node
Example #5
0
    def _locate_raw_data(self):
        """
        Detect location of raw data from which the node has been created

        Find the closest parent node which has raw data defined. In the
        raw data identify the dictionary corresponding to the current
        node, create if needed. Detect the raw data source filename.

        Return tuple with the following three items:

        node_data ... dictionary containing raw data for the current node
        full_data ... full raw data from the closest parent node
        source ... file system path where the full raw data are stored

        """
        # List of node names in the virtual hierarchy
        hierarchy = list()

        # Find the closest parent with raw data defined
        node = self
        while True:
            # Raw data found
            full_data = node._raw_data
            if full_data:
                break
            # No raw data, perhaps a Tree initialized from a dict?
            if not node.parent:
                raise utils.GeneralError(
                    "No raw data found, does the Tree grow on a filesystem?")
            # Extend virtual hierarchy with the current node name, go up
            hierarchy.insert(0, "/" + node.name.rsplit("/")[-1])
            node = node.parent

        # Localize node data dictionary in the virtual hierarchy
        node_data = full_data
        for key in hierarchy:
            # Create a virtual hierarchy level if missing
            if key not in node_data:
                node_data[key] = dict()
            # Initialize as an empty dict if leaf node is empty
            if node_data[key] is None:
                node_data[key] = dict()
            node_data = node_data[key]

        # The full raw data were read from the last source
        return node_data, full_data, node.sources[-1]
Example #6
0
File: base.py Project: jkrysl/fmf
    def __init__(self, data, name=None, parent=None):
        """
        Initialize metadata tree from directory path or data dictionary

        Data parameter can be either a string with directory path to be
        explored or a dictionary with the values already prepared.
        """

        # Bail out if no data and no parent given
        if not data and not parent:
            raise utils.GeneralError(
                "No data or parent provided to initialize the tree.")

        # Initialize family relations, object data and source files
        self.parent = parent
        self.children = dict()
        self.data = dict()
        self.sources = list()
        self.root = None
        self.version = utils.VERSION
        self.original_data = dict()

        # Special handling for top parent
        if self.parent is None:
            self.name = "/"
            if not isinstance(data, dict):
                self._initialize(path=data)
                data = self.root
        # Handle child node creation
        else:
            self.root = self.parent.root
            self.name = os.path.join(self.parent.name, name)
        # Initialize data
        if isinstance(data, dict):
            self.update(data)
        else:
            self.grow(data)
        log.debug("New tree '{0}' created.".format(self))
Example #7
0
    def adjust(self, context, key='adjust', undecided='skip'):
        """
        Adjust tree data based on provided context and rules

        The 'context' should be an instance of the fmf.context.Context
        class describing the environment context. By default, the key
        'adjust' of each node is inspected for possible rules that
        should be applied. Provide 'key' to use a custom key instead.

        Optional 'undecided' parameter can be used to specify what
        should happen when a rule condition cannot be decided because
        context dimension is not defined. By default, such rules are
        skipped. In order to raise the fmf.context.CannotDecide
        exception in such cases use undecided='raise'.
        """

        # Check context sanity
        if not isinstance(context, fmf.context.Context):
            raise utils.GeneralError("Invalid adjust context: '{}'.".format(
                type(context).__name__))

        # Adjust rules should be a dictionary or a list of dictionaries
        try:
            rules = copy.deepcopy(self.data[key])
            log.debug("Applying adjust rules for '{}'.".format(self))
            log.data(rules)
            if isinstance(rules, dict):
                rules = [rules]
            if not isinstance(rules, list):
                raise utils.FormatError(
                    "Invalid adjust rule format in '{}'. "
                    "Should be a dictionary or a list of dictionaries, "
                    "got '{}'.".format(self.name,
                                       type(rules).__name__))
        except KeyError:
            rules = []

        # Check and apply each rule
        for rule in rules:

            # Rule must be a dictionary
            if not isinstance(rule, dict):
                raise utils.FormatError("Adjust rule should be a dictionary.")

            # There must be a condition defined
            try:
                condition = rule.pop('when')
            except KeyError:
                raise utils.FormatError("No condition defined in adjust rule.")

            # The optional 'continue' key should be a bool
            continue_ = rule.pop('continue', True)
            if not isinstance(continue_, bool):
                raise utils.FormatError("The 'continue' value should be bool, "
                                        "got '{}'.".format(continue_))

            # The 'because' key is reserved for optional comments (ignored)
            rule.pop('because', None)

            # Apply remaining rule attributes if context matches
            try:
                if context.matches(condition):
                    self._merge_special(self.data, rule)

                    # First matching rule wins, skip the rest unless continue
                    if not continue_:
                        break
            # Handle undecided rules as requested
            except fmf.context.CannotDecide:
                if undecided == 'skip':
                    continue
                elif undecided == 'raise':
                    raise
                else:
                    raise utils.GeneralError(
                        "Invalid value for the 'undecided' parameter. Should "
                        "be 'skip' or 'raise', got '{}'.".format(undecided))

        # Adjust all child nodes as well
        for child in self.children.values():
            child.adjust(context, key, undecided)