From 06fdbf2a55eca3097b489ae9794c48366e5a93ea Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 2 Dec 2019 16:25:39 +0200 Subject: [PATCH] [Apache v2] Implement find_ancestors (#7561) * Implement find_ancestors * Create the node properly and add assertions * Update certbot-apache/certbot_apache/augeasparser.py Co-Authored-By: ohemorange * Remove comment --- certbot-apache/certbot_apache/apacheparser.py | 8 ++ certbot-apache/certbot_apache/augeasparser.py | 92 ++++++++++++------- certbot-apache/certbot_apache/dualparser.py | 89 ++++++++++-------- certbot-apache/certbot_apache/interfaces.py | 12 +++ .../certbot_apache/tests/augeasnode_test.py | 21 +++++ .../certbot_apache/tests/dualnode_test.py | 9 ++ .../certbot_apache/tests/parsernode_test.py | 4 + 7 files changed, 163 insertions(+), 72 deletions(-) diff --git a/certbot-apache/certbot_apache/apacheparser.py b/certbot-apache/certbot_apache/apacheparser.py index 6625735b4..d9f33f095 100644 --- a/certbot-apache/certbot_apache/apacheparser.py +++ b/certbot-apache/certbot_apache/apacheparser.py @@ -24,6 +24,14 @@ class ApacheParserNode(interfaces.ParserNode): def save(self, msg): # pragma: no cover pass + def find_ancestors(self, name): # pylint: disable=unused-variable + """Find ancestor BlockNodes with a given name""" + return [ApacheBlockNode(name=assertions.PASS, + parameters=assertions.PASS, + ancestor=self, + filepath=assertions.PASS, + metadata=self.metadata)] + class ApacheCommentNode(ApacheParserNode): """ apacheconfig implementation of CommentNode interface """ diff --git a/certbot-apache/certbot_apache/augeasparser.py b/certbot-apache/certbot_apache/augeasparser.py index 3956b1d16..8a3a37083 100644 --- a/certbot-apache/certbot_apache/augeasparser.py +++ b/certbot-apache/certbot_apache/augeasparser.py @@ -75,7 +75,6 @@ from certbot_apache import parser from certbot_apache import parsernode_util as util - class AugeasParserNode(interfaces.ParserNode): """ Augeas implementation of ParserNode interface """ @@ -100,6 +99,63 @@ class AugeasParserNode(interfaces.ParserNode): def save(self, msg): self.parser.save(msg) + def find_ancestors(self, name): + """ + Searches for ancestor BlockNodes with a given name. + + :param str name: Name of the BlockNode parent to search for + + :returns: List of matching ancestor nodes. + :rtype: list of AugeasBlockNode + """ + + ancestors = [] + + parent = self.metadata["augeaspath"] + while True: + # Get the path of ancestor node + parent = parent.rpartition("/")[0] + if not parent: + break + anc = self._create_blocknode(parent) + if anc.name.lower() == name.lower(): + ancestors.append(anc) + + return ancestors + + def _create_blocknode(self, path): + """ + Helper function to create a BlockNode from Augeas path. This is used by + AugeasParserNode.find_ancestors and AugeasBlockNode. + and AugeasBlockNode.find_blocks + + """ + + name = self._aug_get_name(path) + metadata = {"augeasparser": self.parser, "augeaspath": path} + + return AugeasBlockNode(name=name, + ancestor=assertions.PASS, + filepath=apache_util.get_file_path(path), + metadata=metadata) + + def _aug_get_name(self, path): + """ + Helper function to get name of a configuration block or variable from path. + """ + + # Remove the ending slash if any + if path[-1] == "/": # pragma: no cover + path = path[:-1] + + # Get the block name + name = path.split("/")[-1] + + # remove [...], it's not allowed in Apache configuration and is used + # for indexing within Augeas + name = name.split("[")[0] + return name + class AugeasCommentNode(AugeasParserNode): """ Augeas implementation of CommentNode interface """ @@ -263,7 +319,7 @@ class AugeasBlockNode(AugeasDirectiveNode): metadata=new_metadata) return new_comment - def find_blocks(self, name, exclude=True): # pylint: disable=unused-argument + def find_blocks(self, name, exclude=True): """Recursive search of BlockNodes from the sequence of children""" nodes = list() @@ -275,7 +331,7 @@ class AugeasBlockNode(AugeasDirectiveNode): return nodes - def find_directives(self, name, exclude=True): # pylint: disable=unused-argument + def find_directives(self, name, exclude=True): """Recursive search of DirectiveNodes from the sequence of children""" nodes = list() @@ -353,19 +409,6 @@ class AugeasBlockNode(AugeasDirectiveNode): filepath=apache_util.get_file_path(path), metadata=metadata) - def _create_blocknode(self, path): - """Helper function to create a BlockNode from Augeas path""" - - name = self._aug_get_name(path) - metadata = {"augeasparser": self.parser, "augeaspath": path} - - # Because of the dynamic nature, and the fact that we're not populating - # the complete ParserNode tree, we use the search parent as ancestor - return AugeasBlockNode(name=name, - ancestor=assertions.PASS, - filepath=apache_util.get_file_path(path), - metadata=metadata) - def _aug_find_blocks(self, name): """Helper function to perform a search to Augeas DOM tree to search configuration blocks with a given name""" @@ -380,23 +423,6 @@ class AugeasBlockNode(AugeasDirectiveNode): name.lower() in os.path.basename(path).lower()]) return blk_paths - def _aug_get_name(self, path): - """ - Helper function to get name of a configuration block or variable from path. - """ - - # Remove the ending slash if any - if path[-1] == "/": # pragma: no cover - path = path[:-1] - - # Get the block name - name = path.split("/")[-1] - - # remove [...], it's not allowed in Apache configuration and is used - # for indexing within Augeas - name = name.split("[")[0] - return name - def _aug_resolve_child_position(self, name, position): """ Helper function that iterates through the immediate children and figures diff --git a/certbot-apache/certbot_apache/dualparser.py b/certbot-apache/certbot_apache/dualparser.py index ef8e86196..667462d34 100644 --- a/certbot-apache/certbot_apache/dualparser.py +++ b/certbot-apache/certbot_apache/dualparser.py @@ -18,10 +18,59 @@ class DualNodeBase(object): """ Attribute value assertion """ firstval = getattr(self.primary, aname) secondval = getattr(self.secondary, aname) - if not callable(firstval): + exclusions = [ + # Metadata will inherently be different, as ApacheParserNode does + # not have Augeas paths and so on. + aname == "metadata", + callable(firstval) + ] + if not any(exclusions): assertions.assertEqualSimple(firstval, secondval) return firstval + def find_ancestors(self, name): + """ Traverses the ancestor tree and returns ancestors matching name """ + return self._find_helper(DualBlockNode, "find_ancestors", name) + + def _find_helper(self, nodeclass, findfunc, search, **kwargs): + """A helper for find_* functions. The function specific attributes should + be passed as keyword arguments. + + :param interfaces.ParserNode nodeclass: The node class for results. + :param str findfunc: Name of the find function to call + :param str search: The search term + """ + + primary_res = getattr(self.primary, findfunc)(search, **kwargs) + secondary_res = getattr(self.secondary, findfunc)(search, **kwargs) + + # The order of search results for Augeas implementation cannot be + # assured. + + pass_primary = assertions.isPassNodeList(primary_res) + pass_secondary = assertions.isPassNodeList(secondary_res) + new_nodes = list() + + if pass_primary and pass_secondary: + # Both unimplemented + new_nodes.append(nodeclass(primary=primary_res[0], + secondary=secondary_res[0])) # pragma: no cover + elif pass_primary: + for c in secondary_res: + new_nodes.append(nodeclass(primary=primary_res[0], + secondary=c)) + elif pass_secondary: + for c in primary_res: + new_nodes.append(nodeclass(primary=c, + secondary=secondary_res[0])) + else: + assert len(primary_res) == len(secondary_res) + matches = self._create_matching_list(primary_res, secondary_res) + for p, s in matches: + new_nodes.append(nodeclass(primary=p, secondary=s)) + + return new_nodes + class DualCommentNode(DualNodeBase): """ Dual parser implementation of CommentNode interface """ @@ -223,44 +272,6 @@ class DualBlockNode(DualNodeBase): return self._find_helper(DualCommentNode, "find_comments", comment) - def _find_helper(self, nodeclass, findfunc, search, **kwargs): - """A helper for find_* functions. The function specific attributes should - be passed as keyword arguments. - - :param interfaces.ParserNode nodeclass: The node class for results. - :param str findfunc: Name of the find function to call - :param str search: The search term - """ - - primary_res = getattr(self.primary, findfunc)(search, **kwargs) - secondary_res = getattr(self.secondary, findfunc)(search, **kwargs) - - # The order of search results for Augeas implementation cannot be - # assured. - - pass_primary = assertions.isPassNodeList(primary_res) - pass_secondary = assertions.isPassNodeList(secondary_res) - new_nodes = list() - - if pass_primary and pass_secondary: - # Both unimplemented - new_nodes.append(nodeclass(primary=primary_res[0], - secondary=secondary_res[0])) # pragma: no cover - elif pass_primary: - for c in secondary_res: - new_nodes.append(nodeclass(primary=primary_res[0], - secondary=c)) - elif pass_secondary: - for c in primary_res: - new_nodes.append(nodeclass(primary=c, - secondary=secondary_res[0])) - else: - assert len(primary_res) == len(secondary_res) - matches = self._create_matching_list(primary_res, secondary_res) - for p, s in matches: - new_nodes.append(nodeclass(primary=p, secondary=s)) - - return new_nodes def delete_child(self, child): """Deletes a child from the ParserNode implementations. The actual diff --git a/certbot-apache/certbot_apache/interfaces.py b/certbot-apache/certbot_apache/interfaces.py index ecad2d4eb..2d25fad0f 100644 --- a/certbot-apache/certbot_apache/interfaces.py +++ b/certbot-apache/certbot_apache/interfaces.py @@ -192,6 +192,18 @@ class ParserNode(object): """ + @abc.abstractmethod + def find_ancestors(self, name): + """ + Traverses the ancestor tree up, searching for BlockNodes with a specific + name. + + :param str name: Name of the ancestor BlockNode to search for + + :returns: A list of ancestor BlockNodes that match the name + :rtype: list of BlockNode + """ + # Linter rule exclusion done because of https://github.com/PyCQA/pylint/issues/179 @six.add_metaclass(abc.ABCMeta) # pylint: disable=abstract-method diff --git a/certbot-apache/certbot_apache/tests/augeasnode_test.py b/certbot-apache/certbot_apache/tests/augeasnode_test.py index bf01f9fac..043f5d248 100644 --- a/certbot-apache/certbot_apache/tests/augeasnode_test.py +++ b/certbot-apache/certbot_apache/tests/augeasnode_test.py @@ -291,3 +291,24 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public- self.config.parser_root.add_child_directive, "ThisRaisesErrorBecauseMissingParameters" ) + + def test_find_ancestors(self): + vhsblocks = self.config.parser_root.find_blocks("VirtualHost") + macro_test = False + nonmacro_test = False + for vh in vhsblocks: + if "/macro/" in vh.metadata["augeaspath"].lower(): + ancs = vh.find_ancestors("Macro") + self.assertEqual(len(ancs), 1) + macro_test = True + else: + ancs = vh.find_ancestors("Macro") + self.assertEqual(len(ancs), 0) + nonmacro_test = True + self.assertTrue(macro_test) + self.assertTrue(nonmacro_test) + + def test_find_ancestors_bad_path(self): + self.config.parser_root.primary.metadata["augeaspath"] = "" + ancs = self.config.parser_root.primary.find_ancestors("Anything") + self.assertEqual(len(ancs), 0) diff --git a/certbot-apache/certbot_apache/tests/dualnode_test.py b/certbot-apache/certbot_apache/tests/dualnode_test.py index bdfab4fc7..9e5a5e9aa 100644 --- a/certbot-apache/certbot_apache/tests/dualnode_test.py +++ b/certbot-apache/certbot_apache/tests/dualnode_test.py @@ -412,3 +412,12 @@ class DualParserNodeTest(unittest.TestCase): # pylint: disable=too-many-public- self.assertFalse(self.block == ne_block) self.assertFalse(self.directive == ne_directive) self.assertFalse(self.comment == ne_comment) + + def test_find_ancestors(self): + primarymock = mock.MagicMock(return_value=[]) + secondarymock = mock.MagicMock(return_value=[]) + self.block.primary.find_ancestors = primarymock + self.block.secondary.find_ancestors = secondarymock + self.block.find_ancestors("anything") + self.assertTrue(primarymock.called) + self.assertTrue(secondarymock.called) diff --git a/certbot-apache/certbot_apache/tests/parsernode_test.py b/certbot-apache/certbot_apache/tests/parsernode_test.py index 1a2288c82..a6caf4814 100644 --- a/certbot-apache/certbot_apache/tests/parsernode_test.py +++ b/certbot-apache/certbot_apache/tests/parsernode_test.py @@ -24,6 +24,10 @@ class DummyParserNode(interfaces.ParserNode): """Save""" pass + def find_ancestors(self, name): # pragma: no cover + """ Find ancestors """ + return [] + class DummyCommentNode(DummyParserNode): """ A dummy class implementing CommentNode interface """