Compare commits

...

5 Commits

Author SHA1 Message Date
Joona Hoikkala
a5d1509434 ParserNode implementation of VirtualHost creation 2019-08-14 00:04:40 +03:00
Joona Hoikkala
270754deff [Apache v2] New ParserNode interface abstraction (#7246)
* New ParserNode interface abstraction

* Add the test assertions, and fix interface

* Update certbot-apache/certbot_apache/interfaces.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update certbot-apache/certbot_apache/interfaces.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update certbot-apache/certbot_apache/interfaces.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Add dummy tests and change arguments to properties

* Add more context to comment property docstring

* Add documentation to the main docstring

* Streamline the parameter naming

* Update certbot-apache/certbot_apache/interfaces.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Add explicit instructions how whitespaces are treated in set_parameters

* Add the information about lookups being case insensitive.

* Add context about whitespacing to add_ - methods

* Update certbot-apache/certbot_apache/interfaces.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update certbot-apache/certbot_apache/interfaces.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-08-13 09:22:51 -07:00
Joona Hoikkala
a83f9eb4e4 Merge pull request #7321 from certbot/fix-apache-parser-v2-cover
Fix apache-parser-v2 tests
2019-08-13 15:28:24 +03:00
Brad Warren
fed2264dac Merge branch 'master' into fix-apache-parser-v2-cover 2019-08-09 11:21:15 -07:00
Brad Warren
31a8d086fc Merge pull request #7289 from certbot/fix-apache-parser-v2
Fix AppVeyor on the apache-parser-v2 branch
2019-08-02 11:22:53 -07:00
5 changed files with 970 additions and 1 deletions

View File

@@ -0,0 +1,192 @@
""" Tests for ParserNode interface """
from certbot_apache import interfaces
from acme.magic_typing import Dict, Tuple # pylint: disable=unused-import, no-name-in-module
class AugeasCommentNode(interfaces.CommentNode):
""" Augeas implementation of CommentNode interface """
ancestor = None
comment = ""
dirty = False
_metadata = dict() # type: Dict[str, object]
def __init__(self, comment, ancestor=None):
self.comment = comment
self.ancestor = ancestor
def save(self, msg): # pragma: no cover
pass
# Apache specific functionality
def get_metadata(self, key):
""" Returns a metadata object
:param str key: Metadata object name to return
:returns: Requested metadata object
"""
try:
return self._metadata[key]
except KeyError:
return None
class AugeasDirectiveNode(interfaces.DirectiveNode):
""" Augeas implementation of DirectiveNode interface """
ancestor = None
parameters = tuple() # type: Tuple[str, ...]
dirty = False
enabled = True
name = ""
_metadata = dict() # type: Dict[str, object]
def __init__(self, name, parameters=tuple(), ancestor=None):
self.name = name
self.parameters = parameters
self.ancestor = ancestor
def save(self, msg): # pragma: no cover
pass
def set_parameters(self, parameters): # pragma: no cover
self.parameters = tuple("CERTBOT_PASS_ASSERT")
# Apache specific functionality
def get_filename(self):
"""Returns the filename where this directive exists on disk
:returns: File path to this node.
:rtype: str
"""
# Following is the real implementation when everything else is in place:
# return apache_util.get_file_path(
# self.parser.aug.get("/augeas/files%s/path" % apache_util.get_file_path(path)))
return "CERTBOT_PASS_ASSERT"
def get_metadata(self, key):
""" Returns a metadata object
:param str key: Metadata object name to return
:returns: Requested metadata object
"""
try:
return self._metadata[key]
except KeyError:
return None
def has_parameter(self, parameter, position=None):
"""Checks if this ParserNode object has a supplied parameter. This check
is case insensitive.
:param str parameter: Parameter value to look for
:param position: Optional explicit position of parameter to look for
:returns: True if parameter is found
:rtype: bool
"""
if position != None:
return parameter.lower() == self.parameters[position].lower()
for param in self.parameters:
if param.lower() == parameter.lower():
return True
return False
class AugeasBlockNode(interfaces.BlockNode):
""" Augeas implementation of BlockNode interface """
ancestor = None
parameters = tuple() # type: Tuple[str, ...]
children = tuple() # type: Tuple[interfaces.ParserNode, ...]
dirty = False
enabled = True
name = ""
_metadata = dict() # type: Dict[str, object]
def __init__(self, name, parameters=tuple(), ancestor=None):
self.name = name
self.parameters = parameters
self.ancestor = ancestor
def save(self, msg): # pragma: no cover
pass
def add_child_block(self, name, parameters=None, position=None): # pragma: no cover
new_block = AugeasBlockNode("CERTBOT_PASS_ASSERT", ancestor=self)
self.children += (new_block,)
return new_block
def add_child_directive(self, name, parameters=None, position=None): # pragma: no cover
new_dir = AugeasDirectiveNode("CERTBOT_PASS_ASSERT", ancestor=self)
self.children += (new_dir,)
return new_dir
def add_child_comment(self, comment="", position=None): # pragma: no cover
new_comment = AugeasCommentNode("CERTBOT_PASS_ASSERT", ancestor=self)
self.children += (new_comment,)
return new_comment
def find_blocks(self, name, exclude=True): # pragma: no cover
return [AugeasBlockNode("CERTBOT_PASS_ASSERT", ancestor=self)]
def find_directives(self, name, exclude=True): # pragma: no cover
return [AugeasDirectiveNode("CERTBOT_PASS_ASSERT", ancestor=self)]
def find_comments(self, comment, exact=False): # pragma: no cover
return [AugeasCommentNode("CERTBOT_PASS_ASSERT", ancestor=self)]
def delete_child(self, child): # pragma: no cover
pass
def set_parameters(self, parameters): # pragma: no cover
self.parameters = tuple("CERTBOT_PASS_ASSERT")
def unsaved_files(self): # pragma: no cover
return ["CERTBOT_PASS_ASSERT"]
# Apache specific functionality
def get_filename(self):
"""Returns the filename where this directive exists on disk
:returns: File path to this node.
:rtype: str
"""
# Following is the real implementation when everything else is in place:
# return apache_util.get_file_path(
# self.parser.aug.get("/augeas/files%s/path" %
# apache_util.get_file_path(self.get_metadata("augeas_path")))
return "CERTBOT_PASS_ASSERT"
def get_metadata(self, key):
""" Returns a metadata object
:param str key: Metadata object name to return
:returns: Requested metadata object
"""
try:
return self._metadata[key]
except KeyError:
return None
def has_parameter(self, parameter, position=None):
"""Checks if this ParserNode object has a supplied parameter. This check
is case insensitive.
:param str parameter: Parameter value to look for
:param position: Optional explicit position of parameter to look for
:returns: True if parameter is found
:rtype: bool
"""
if position != None:
return parameter.lower() == self.parameters[position].lower()
for param in self.parameters:
if param.lower() == parameter.lower():
return True
return False

View File

@@ -810,6 +810,28 @@ class ApacheConfigurator(common.Installer):
return (servername, serveraliases)
def _populate_vhost_names_v2(self, vhost):
"""Helper function that populates the VirtualHost names.
:param host: In progress vhost whose names will be added
:type host: :class:`~certbot_apache.obj.VirtualHost`
"""
servername_match = vhost.node.find_directives("ServerName",
exclude=False)
serveralias_match = vhost.node.find_directives("ServerAlias",
exclude=False)
if servername_match:
servername = servername_match[-1].parameters[-1]
if not vhost.modmacro:
for alias in serveralias_match:
for serveralias in alias.parameters:
vhost.aliases.add(serveralias)
vhost.name = servername
def _add_servernames(self, host):
"""Helper function for get_virtual_hosts().
@@ -871,6 +893,52 @@ class ApacheConfigurator(common.Installer):
self._add_servernames(vhost)
return vhost
def _create_vhost_v2(self, node):
"""Used by get_virtual_hosts to create vhost objects using ParserNode
interfaces.
:param interfaces.BlockNode node: The BlockNode object of VirtualHost block
:returns: newly created vhost
:rtype: :class:`~certbot_apache.obj.VirtualHost`
"""
addrs = set()
for param in node.parameters:
addrs.add(obj.Addr.fromstring(param))
is_ssl = False
sslengine = node.find_directives("SSLEngine")
if sslengine:
for directive in sslengine:
# TODO: apache-parser-v2
# This search should be made wiser. (using other identificators)
if directive.has_parameter("on", 0):
is_ssl = True
# "SSLEngine on" might be set outside of <VirtualHost>
# Treat vhosts with port 443 as ssl vhosts
for addr in addrs:
if addr.get_port() == "443":
is_ssl = True
filename = node.get_filename()
if filename is None:
return None
macro = False
if node.find_directives("Macro"):
macro = True
vhost_enabled = self.parser.parsed_in_original(filename)
vhost = obj.VirtualHost(filename, node.get_metadata("augeas_path"),
addrs, is_ssl, vhost_enabled, modmacro=macro,
node=node)
self._populate_vhost_names_v2(vhost)
return vhost
def get_virtual_hosts(self):
"""Returns list of virtual hosts found in the Apache configuration.

View File

@@ -0,0 +1,476 @@
"""ParserNode interface for interacting with configuration tree.
General description
-------------------
The ParserNode interfaces are designed to be able to contain all the parsing logic,
while allowing their users to interact with the configuration tree in a Pythonic
and well structured manner.
The structure allows easy traversal of the tree of ParserNodes. Each ParserNode
stores a reference to its ancestor and immediate children, allowing the user to
traverse the tree using built in interface methods as well as accessing the interface
properties directly.
ParserNode interface implementation should stand between the actual underlying
parser functionality and the business logic within Configurator code, interfacing
with both. The ParserNode tree is a result of configuration parsing action.
ParserNode tree will be in charge of maintaining the parser state and hence the
abstract syntax tree (AST). Interactions between ParserNode tree and underlying
parser should involve only parsing the configuration files to this structure, and
writing it back to the filesystem - while preserving the format including whitespaces.
For some implementations (Apache for example) it's important to keep track of and
to use state information while parsing conditional blocks and directives. This
allows the implementation to set a flag to parts of the parsed configuration
structure as not being in effect in a case of unmatched conditional block. It's
important to store these blocks in the tree as well in order to not to conduct
destructive actions (failing to write back parts of the configuration) while writing
the AST back to the filesystem.
The ParserNode tree is in charge of maintaining the its own structure while every
child node fetched with find - methods or by iterating its list of children can be
changed in place. When making changes the affected nodes should be flagged as "dirty"
in order for the parser implementation to figure out the parts of the configuration
that need to be written back to disk during the save() operation.
Metadata
--------
The metadata holds all the implementation specific attributes of the ParserNodes -
things like the positional information related to the AST, file paths, whitespacing,
and any other information relevant to the underlying parser engine.
Access to the metadata should be handled by implementation specific methods, allowing
the Configurator functionality to access the underlying information where needed.
A good example of this is file path of a node - something that is needed by the
reverter functionality within the Configurator.
Apache implementation
---------------------
The Apache implementation of ParserNode interface requires some implementation
specific functionalities that are not described by the interface itself.
Conditional blocks
Apache configuration can have conditional blocks, for example: <IfModule ...>,
resulting the directives and subblocks within it being either enabled or disabled.
While find_* interface methods allow including the disabled parts of the configuration
tree in searches a special care needs to be taken while parsing the structure in
order to reflect the active state of configuration.
Whitespaces
Each ParserNode object is responsible of storing its prepending whitespace characters
in order to be able to write the AST back to filesystem like it was, preserving the
format, this applies for parameters of BlockNode and DirectiveNode as well.
When parameters of ParserNode are changed, the pre-existing whitespaces in the
parameter sequence are discarded, as the general reason for storing them is to
maintain the ability to write the configuration back to filesystem exactly like
it was. This loses its meaning when we have to change the directives or blocks
parameters for other reasons.
Searches and matching
Apache configuration is largely case insensitive, so the Apache implementation of
ParserNode interface needs to provide the user means to match block and directive
names and parameters in case insensitive manner. This does not apply to everything
however, for example the parameters of a conditional statement may be case sensitive.
For this reason the internal representation of data should not ignore the case.
"""
import abc
import six
@six.add_metaclass(abc.ABCMeta)
class ParserNode(object):
"""
ParserNode is the basic building block of the tree of such nodes,
representing the structure of the configuration. It is largely meant to keep
the structure information intact and idiomatically accessible.
The root node as well as the child nodes of it should be instances of ParserNode.
Nodes keep track of their differences to on-disk representation of configuration
by marking modified ParserNodes as dirty to enable partial write-to-disk for
different files in the configuration structure.
While for the most parts the usage and the child types are obvious, "include"-
and similar directives are an exception to this rule. This is because of the
nature of include directives - which unroll the contents of another file or
configuration block to their place. While we could unroll the included nodes
to the parent tree, it remains important to keep the context of include nodes
separate in order to write back the original configuration as it was.
For parsers that require the implementation to keep track of the whitespacing,
it's responsibility of each ParserNode object itself to store its prepending
whitespaces in order to be able to reconstruct the complete configuration file
as it was when originally read from the disk.
"""
@property
@abc.abstractmethod
def ancestor(self): # pragma: no cover
"""
This property contains a reference to ancestor node, or None if the node
is the root node of the configuration tree.
:returns: The ancestor BlockNode object, or None for root node.
:rtype: ParserNode
"""
raise NotImplementedError
@property
@abc.abstractmethod
def dirty(self): # pragma: no cover
"""
This property contains a boolean value of the information if this node has
been modified since last save (or after the initial parse).
:returns: True if this node has had changes that have not yet been written
to disk.
:rtype: bool
"""
raise NotImplementedError
@abc.abstractmethod
def save(self, msg):
"""
Save traverses the children, and attempts to write the AST to disk for
all the objects that are marked dirty. The actual operation of course
depends on the underlying implementation. save() shouldn't be called
from the Configurator outside of its designated save() method in order
to ensure that the Reverter checkpoints are created properly.
Note: this approach of keeping internal structure of the configuration
within the ParserNode tree does not represent the file inclusion structure
of actual configuration files that reside in the filesystem. To handle
file writes properly, the file specific temporary trees should be extracted
from the full ParserNode tree where necessary when writing to disk.
"""
@six.add_metaclass(abc.ABCMeta)
class CommentNode(ParserNode):
"""
CommentNode class is used for representation of comments within the parsed
configuration structure. Because of the nature of comments, it is not able
to have child nodes and hence it is always treated as a leaf node.
CommentNode stores its contents in class variable 'comment' and does not
have a specific name.
"""
@property
@abc.abstractmethod
def comment(self): # pragma: no cover
"""
Comment property contains the contents of the comment without the comment
directives (typically # or /* ... */).
:returns: A string containing the comment
:rtype: str
"""
raise NotImplementedError
@six.add_metaclass(abc.ABCMeta)
class DirectiveNode(ParserNode):
"""
DirectiveNode class represents a configuration directive within the configuration.
It can have zero or more parameters attached to it. Because of the nature of
single directives, it is not able to have child nodes and hence it is always
treated as a leaf node.
"""
@property
@abc.abstractmethod
def enabled(self): # pragma: no cover
"""
Configuration blocks may have conditional statements enabling or disabling
their contents. This property returns the state of this DirectiveNode.
:returns: True if the DirectiveNode is parsed and enabled in the configuration.
:rtype: bool
"""
raise NotImplementedError
@property
@abc.abstractmethod
def name(self): # pragma: no cover
"""
Name property contains the name of the directive.
:returns: Name of this node
:rtype: str
"""
raise NotImplementedError
@property
@abc.abstractmethod
def parameters(self): # pragma: no cover
"""
This property contains a tuple of parameters of this ParserNode object
excluding whitespaces.
:returns: A tuple of parameters for this node
:rtype: tuple
"""
raise NotImplementedError
@abc.abstractmethod
def set_parameters(self, parameters):
"""
Sets the sequence of parameters for this ParserNode object without
whitespaces. While the whitespaces for parameters are discarded when using
this method, the whitespacing preceeding the ParserNode itself should be
kept intact.
:param list parameters: sequence of parameters
"""
@six.add_metaclass(abc.ABCMeta)
class BlockNode(ParserNode):
"""
BlockNode class represents a block of nested configuration directives, comments
and other blocks as its children. A BlockNode can have zero or more parameters
attached to it.
Configuration blocks typically consist of one or more child nodes of all possible
types. Because of this, the BlockNode class has various discovery and structure
management methods.
Lists of parameters used as an optional argument for some of the methods should
be lists of strings that are applicable parameters for each specific BlockNode
or DirectiveNode type. As an example, for a following configuration example:
<VirtualHost *:80>
...
</VirtualHost>
The node type would be BlockNode, name would be 'VirtualHost' and its parameters
would be: ['*:80'].
While for the following example:
LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so
The node type would be DirectiveNode, name would be 'LoadModule' and its
parameters would be: ['alias_module', '/usr/lib/apache2/modules/mod_alias.so']
The applicable parameters are dependent on the underlying configuration language
and its grammar.
"""
@abc.abstractmethod
def add_child_block(self, name, parameters=None, position=None):
"""
Adds a new BlockNode child node with provided values and marks the callee
BlockNode dirty. This is used to add new children to the AST. The preceeding
whitespaces should not be added based on the ancestor or siblings for the
newly created object. This is to match the current behavior of the legacy
parser implementation.
:param str name: The name of the child node to add
:param list parameters: list of parameters for the node
:param int position: Position in the list of children to add the new child
node to. Defaults to None, which appends the newly created node to the list.
If an integer is given, the child is inserted before that index in the
list similar to list().insert.
:returns: BlockNode instance of the created child block
"""
@abc.abstractmethod
def add_child_directive(self, name, parameters=None, position=None):
"""
Adds a new DirectiveNode child node with provided values and marks the
callee BlockNode dirty. This is used to add new children to the AST. The
preceeding whitespaces should not be added based on the ancestor or siblings
for the newly created object. This is to match the current behavior of the
legacy parser implementation.
:param str name: The name of the child node to add
:param list parameters: list of parameters for the node
:param int position: Position in the list of children to add the new child
node to. Defaults to None, which appends the newly created node to the list.
If an integer is given, the child is inserted before that index in the
list similar to list().insert.
:returns: DirectiveNode instance of the created child directive
"""
@abc.abstractmethod
def add_child_comment(self, comment="", position=None):
"""
Adds a new CommentNode child node with provided value and marks the
callee BlockNode dirty. This is used to add new children to the AST. The
preceeding whitespaces should not be added based on the ancestor or siblings
for the newly created object. This is to match the current behavior of the
legacy parser implementation.
:param str comment: Comment contents
:param int position: Position in the list of children to add the new child
node to. Defaults to None, which appends the newly created node to the list.
If an integer is given, the child is inserted before that index in the
list similar to list().insert.
:returns: CommentNode instance of the created child comment
"""
@property
@abc.abstractmethod
def children(self): # pragma: no cover
"""
This property contains a list ParserNode objects that are the children
for this node. The order of children is the same as that of the parsed
configuration block.
:returns: A tuple of this block's children
:rtype: tuple
"""
raise NotImplementedError
@property
@abc.abstractmethod
def enabled(self):
"""
Configuration blocks may have conditional statements enabling or disabling
their contents. This property returns the state of this configuration block.
In case of unmatched conditional statement in block, this block itself should
be set enabled while its children should be set disabled.
:returns: True if the BlockNode is parsed and enabled in the configuration.
:rtype: bool
"""
@abc.abstractmethod
def find_blocks(self, name, exclude=True):
"""
Find a configuration block by name. This method walks the child tree of
ParserNodes under the instance it was called from. This way it is possible
to search for the whole configuration tree, when starting from root node or
to do a partial search when starting from a specified branch. The lookup
should be case insensitive.
:param str name: The name of the directive to search for
:param bool exclude: If the search results should exclude the contents of
ParserNode objects that reside within conditional blocks and because
of current state are not enabled.
:returns: A list of found BlockNode objects.
"""
@abc.abstractmethod
def find_directives(self, name, exclude=True):
"""
Find a directive by name. This method walks the child tree of ParserNodes
under the instance it was called from. This way it is possible to search
for the whole configuration tree, when starting from root node, or to do
a partial search when starting from a specified branch. The lookup should
be case insensitive.
:param str name: The name of the directive to search for
:param bool exclude: If the search results should exclude the contents of
ParserNode objects that reside within conditional blocks and because
of current state are not enabled.
:returns: A list of found DirectiveNode objects.
"""
@abc.abstractmethod
def find_comments(self, comment, exact=False):
"""
Find comments with value containing or being exactly the same as search term.
This method walks the child tree of ParserNodes under the instance it was
called from. This way it is possible to search for the whole configuration
tree, when starting from root node, or to do a partial search when starting
from a specified branch. The lookup should be case sensitive.
:param str comment: The content of comment to search for
:param bool exact: If the comment needs to exactly match the search term
:returns: A list of found CommentNode objects.
"""
@abc.abstractmethod
def delete_child(self, child):
"""
Remove a specified child node from the list of children of the called
BlockNode object.
:param ParserNode child: Child ParserNode object to remove from the list
of children of the callee.
"""
@property
@abc.abstractmethod
def name(self): # pragma: no cover
"""
Name property contains the name of the block. As an example for config:
<VirtualHost *:80> ... </VirtualHost>
the name would be "VirtualHost".
:returns: Name of this node
:rtype: str
"""
raise NotImplementedError
@property
@abc.abstractmethod
def parameters(self): # pragma: no cover
"""
This property contains a tuple of parameters of this ParserNode object
excluding whitespaces.
:returns: A tuple of parameters for this node
:rtype: tuple
"""
raise NotImplementedError
@abc.abstractmethod
def set_parameters(self, parameters):
"""
Sets the sequence of parameters for this ParserNode object without
whitespaces. While the whitespaces for parameters are discarded when using
this method, the whitespacing preceeding the ParserNode itself should be
kept intact.
:param list parameters: sequence of parameters
"""
@abc.abstractmethod
def unsaved_files(self):
"""
Returns a list of file paths that have been changed since the last save
(or the initial configuration parse). The intended use for this method
is to tell the Reverter which files need to be included in a checkpoint.
This is typically called for the root of the ParserNode tree.
:returns: list of file paths of files that have been changed but not yet
saved to disk.
"""

View File

@@ -124,7 +124,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
def __init__(self, filep, path, addrs, ssl, enabled, name=None,
aliases=None, modmacro=False, ancestor=None):
aliases=None, modmacro=False, ancestor=None, node=None):
# pylint: disable=too-many-arguments
"""Initialize a VH."""
@@ -137,6 +137,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
self.enabled = enabled
self.modmacro = modmacro
self.ancestor = ancestor
self.node = node
def get_names(self):
"""Return a set of all names."""

View File

@@ -0,0 +1,232 @@
""" Tests for ParserNode interface """
import unittest
import mock
from acme.magic_typing import Dict, Tuple # pylint: disable=unused-import, no-name-in-module
from certbot_apache import augeasparser
from certbot_apache import interfaces
from certbot_apache.tests import util
class DummyCommentNode(interfaces.CommentNode):
""" A dummy class implementing CommentNode interface """
ancestor = None
comment = ""
dirty = False
def save(self, msg): # pragma: no cover
pass
class DummyDirectiveNode(interfaces.DirectiveNode):
""" A dummy class implementing DirectiveNode interface """
ancestor = None
parameters = tuple() # type: Tuple[str, ...]
dirty = False
enabled = True
name = ""
def save(self, msg): # pragma: no cover
pass
def set_parameters(self, parameters): # pragma: no cover
pass
class DummyBlockNode(interfaces.BlockNode):
""" A dummy class implementing BlockNode interface """
ancestor = None
parameters = tuple() # type: Tuple[str, ...]
children = tuple() # type: Tuple[interfaces.ParserNode, ...]
dirty = False
enabled = True
name = ""
def save(self, msg): # pragma: no cover
pass
def add_child_block(self, name, parameters=None, position=None): # pragma: no cover
pass
def add_child_directive(self, name, parameters=None, position=None): # pragma: no cover
pass
def add_child_comment(self, comment="", position=None): # pragma: no cover
pass
def find_blocks(self, name, exclude=True): # pragma: no cover
pass
def find_directives(self, name, exclude=True): # pragma: no cover
pass
def find_comments(self, comment, exact=False): # pragma: no cover
pass
def delete_child(self, child): # pragma: no cover
pass
def set_parameters(self, parameters): # pragma: no cover
pass
def unsaved_files(self): # pragma: no cover
pass
class ParserNodeTest(util.ApacheTest):
"""Test cases for ParserNode interface"""
def __init__(self, *args, **kwargs):
super(ParserNodeTest, self).__init__(*args, **kwargs)
self.mock_nodes = dict() # type: Dict[str, interfaces.ParserNode]
def setUp(self): # pylint: disable=arguments-differ
super(ParserNodeTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
def test_dummy(self):
dummyblock = DummyBlockNode()
dummydirective = DummyDirectiveNode()
dummycomment = DummyCommentNode()
def _create_mock_vhost_nodes(self, servername, serveraliases, addrs):
"""Create a mock VirtualHost nodes"""
nodes = {
"VirtualHost": augeasparser.AugeasBlockNode("VirtualHost", tuple(addrs)),
"ServerName": augeasparser.AugeasDirectiveNode("ServerName",
(servername,)),
"ServerAlias": augeasparser.AugeasDirectiveNode("ServerAlias",
tuple(serveraliases)),
"Macro": augeasparser.AugeasDirectiveNode("Macro", ("variable", "value",)),
"SSLEngine": augeasparser.AugeasDirectiveNode("SSLEngine", ("on",))
}
return nodes
def mock_find_directives(self, name, exclude=True): # pylint: disable=unused-argument
"""
Mocks BlockNode.find_directives() and returns values defined in class
variable self.mock_nodes, set by the test case
"""
try:
return self.mock_nodes[name]
except KeyError:
return []
def test_create_vhost_v2_nonssl(self):
nodes = self._create_mock_vhost_nodes("example.com",
["a1.example.com", "a2.example.com"],
["*:80"])
nodes["VirtualHost"].find_directives = self.mock_find_directives
self.mock_nodes = {"ServerName": [nodes["ServerName"]],
"ServerAlias": [nodes["ServerAlias"]]}
vhost = self.config._create_vhost_v2(nodes["VirtualHost"]) # pylint: disable=protected-access
self.assertEqual(vhost.name, "example.com")
self.assertTrue("a1.example.com" in vhost.aliases)
self.assertTrue("a2.example.com" in vhost.aliases)
self.assertEqual(len(vhost.aliases), 2)
self.assertEqual(len(vhost.addrs), 1)
self.assertFalse(vhost.ssl)
self.assertFalse(vhost.modmacro)
def test_create_vhost_v2_macro(self):
nodes = self._create_mock_vhost_nodes("example.com",
["a1.example.com", "a2.example.com"],
["*:80"])
nodes["VirtualHost"].find_directives = self.mock_find_directives
self.mock_nodes = {"ServerName": [nodes["ServerName"]],
"ServerAlias": [nodes["ServerAlias"]],
"Macro": [nodes["Macro"]]}
vhost = self.config._create_vhost_v2(nodes["VirtualHost"]) # pylint: disable=protected-access
self.assertEqual(vhost.name, None)
self.assertEqual(vhost.aliases, set())
self.assertFalse(vhost.ssl)
self.assertTrue(vhost.modmacro)
def test_create_vhost_v2_ssl_port(self):
nodes = self._create_mock_vhost_nodes("example.com",
["a1.example.com", "a2.example.com"],
["*:443"])
nodes["VirtualHost"].find_directives = self.mock_find_directives
self.mock_nodes = {"ServerName": [nodes["ServerName"]],
"ServerAlias": [nodes["ServerAlias"]]}
vhost = self.config._create_vhost_v2(nodes["VirtualHost"]) # pylint: disable=protected-access
self.assertTrue(vhost.ssl)
self.assertFalse(vhost.modmacro)
def test_create_vhost_v2_sslengine(self):
nodes = self._create_mock_vhost_nodes("example.com",
["a1.example.com", "a2.example.com"],
["*:80"])
nodes["VirtualHost"].find_directives = self.mock_find_directives
self.mock_nodes = {"ServerName": [nodes["ServerName"]],
"ServerAlias": [nodes["ServerAlias"]],
"SSLEngine": [nodes["SSLEngine"]]}
vhost = self.config._create_vhost_v2(nodes["VirtualHost"]) # pylint: disable=protected-access
self.assertTrue(vhost.ssl)
self.assertFalse(vhost.modmacro)
def test_create_vhost_v2_no_filename(self):
nodes = self._create_mock_vhost_nodes("example.com",
["a1.example.com", "a2.example.com"],
["*:80"])
nodes["VirtualHost"].find_directives = self.mock_find_directives
self.mock_nodes = {"ServerName": [nodes["ServerName"]],
"ServerAlias": [nodes["ServerAlias"]],
"SSLEngine": [nodes["SSLEngine"]]}
filename = "certbot_apache.augeasparser.AugeasBlockNode.get_filename"
with mock.patch(filename) as mock_filename:
mock_filename.return_value = None
vhost = self.config._create_vhost_v2(nodes["VirtualHost"]) # pylint: disable=protected-access
self.assertEqual(vhost, None)
def test_comment_node_creation(self):
comment = augeasparser.AugeasCommentNode("This is a comment")
comment._metadata["augeas_path"] = "/whatever" # pylint: disable=protected-access
self.assertEqual(comment.get_metadata("augeas_path"), "/whatever")
self.assertEqual(comment.get_metadata("something_else"), None)
self.assertEqual(comment.comment, "This is a comment")
def test_directive_node_creation(self):
directive = augeasparser.AugeasDirectiveNode("DIRNAME", ("p1", "p2",))
directive._metadata["augeas_path"] = "/whatever" # pylint: disable=protected-access
self.assertEqual(directive.get_metadata("augeas_path"), "/whatever")
self.assertEqual(directive.get_metadata("something_else"), None)
self.assertEqual(directive.name, "DIRNAME")
self.assertEqual(directive.parameters, ("p1", "p2",))
self.assertTrue(directive.has_parameter("P1", 0))
self.assertFalse(directive.has_parameter("P2", 0))
self.assertFalse(directive.has_parameter("P3"))
self.assertTrue(directive.has_parameter("P2"))
self.assertEqual(directive.get_filename(), "CERTBOT_PASS_ASSERT")
def test_block_node_creation(self):
block = augeasparser.AugeasBlockNode("BLOCKNAME", ("first", "SECOND",))
block._metadata["augeas_path"] = "/whatever" # pylint: disable=protected-access
self.assertEqual(block.get_metadata("augeas_path"), "/whatever")
self.assertEqual(block.get_metadata("something_else"), None)
self.assertEqual(block.name, "BLOCKNAME")
self.assertEqual(block.parameters, ("first", "SECOND",))
self.assertFalse(block.has_parameter("second", 0))
self.assertFalse(block.has_parameter("SECOND", 0))
self.assertFalse(block.has_parameter("third"))
self.assertTrue(block.has_parameter("FIRST"))
self.assertEqual(block.get_filename(), "CERTBOT_PASS_ASSERT")
if __name__ == "__main__":
unittest.main() # pragma: no cover