Implement find_ancestors

This commit is contained in:
Joona Hoikkala 2019-11-14 11:38:57 +02:00
parent 517ff5cb19
commit d1de5a9d5e
No known key found for this signature in database
GPG key ID: D5AA86BBF9B29A5C
6 changed files with 115 additions and 33 deletions

View file

@ -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,65 @@ class AugeasParserNode(interfaces.ParserNode):
def save(self, msg):
self.parser.save(msg)
def find_ancestors(self, name):
"""
Searches 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}
# 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_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 """
@ -253,7 +311,7 @@ class AugeasBlockNode(AugeasDirectiveNode):
self.children += (new_comment,)
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()
@ -265,7 +323,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()
@ -343,19 +401,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"""
@ -370,23 +415,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

View file

@ -21,6 +21,14 @@ class DualNodeBase(object):
assertions.assertEqualSimple(firstval, secondval)
return firstval
def find_ancestors(self, name):
""" Traverses the ancestor tree and returns ancestors matching name """
primary_ancs = self.primary.find_ancestors(name)
secondary_ancs = self.secondary.find_ancestors(name)
assert primary_ancs == secondary_ancs
return primary_ancs
class DualCommentNode(DualNodeBase):
""" Dual parser implementation of CommentNode interface """

View file

@ -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

View file

@ -281,3 +281,24 @@ class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-
self.config.parser_root.primary.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)

View file

@ -415,3 +415,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)

View file

@ -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 """