"""
Combiner for httpd configurations
=================================
Combiner for parsing part of httpd configurations. It collects all HttpdConf
generated from each configuration file and combines them to expose a
consolidated configuration tree.
.. note::
At this point in time, you should **NOT** filter the httpd configurations
to avoid finding directives in incorrect sections.
"""
import six
import string
from insights.contrib.ipaddress import ip_address, ip_network
from collections import namedtuple
from insights import run
from insights.core import ConfigCombiner, ConfigParser
from insights.core.plugins import combiner, parser
from insights.parsr.query import (Directive, Entry, pred, pred2, Section,
startswith)
from insights.parsr import (Char, EOF, EOL, EndTagName, Forward, FS, GT, InSet,
Literal, LT, Letters, Lift, LineEnd, Many, Number, OneLineComment,
PosMarker, QuotedString, skip_none, StartTagName, String, WS, WSChar)
from insights.parsers.httpd_conf import HttpdConf, dict_deep_merge, ParsedData
from insights.specs import Specs
from insights.util import deprecated
[docs]@combiner(HttpdConf)
class HttpdConfAll(object):
"""
.. warning::
This combiner class is deprecated, please use
:py:class:`insights.combiners.httpd_conf.HttpdConfTree` instead.
A combiner for parsing all httpd configurations. It parses all sources and makes a composition
to store actual loaded values of the settings as well as information about parsed configuration
files and raw values.
Note:
``ParsedData`` is a named tuple with the following properties:
- ``value`` - the value of the option.
- ``line`` - the complete line as found in the config file.
- ``section`` - the section type that the option belongs to.
- ``section_name`` - the section name that the option belongs to.
- ``file_name`` - the config file name.
- ``file_path`` - the complete config file path.
``ConfigData`` is a named tuple with the following properties:
- ``file_name`` - the config file name.
- ``file_path`` - the complete config file path.
- ``data_dict`` - original data dictionary from parser.
Attributes:
data (dict): Dictionary of parsed settings in format {option: [ParsedData, ParsedData]}.
It stores a list of parsed values, usually only the last value is needed,
except situations when directives which can use selective overriding,
such as ``UserDir``, are used.
config_data (list): List of parsed config files in containing ConfigData named tuples.
"""
ConfigData = namedtuple('ConfigData', ['file_name', 'file_path', 'full_data_dict'])
def __init__(self, httpd_conf):
deprecated(HttpdConfAll, "Import HttpdConfTree from 'insights.combiners.httpd_conf' instead.")
self.data = {}
self.config_data = []
config_files_data = []
main_config_data = []
for httpd_parser in httpd_conf:
file_name = httpd_parser.file_name
file_path = httpd_parser.file_path
# Flag to be used for different handling of the main config file
main_config = httpd_parser.file_name == 'httpd.conf'
if not main_config:
config_files_data.append(self.ConfigData(file_name, file_path,
httpd_parser.data))
else:
main_config_data.append(self.ConfigData(file_name, file_path,
httpd_parser.first_half))
main_config_data.append(self.ConfigData(file_name, file_path,
httpd_parser.second_half))
# Sort configuration files
config_files_data.sort()
# Add both parts of main configuration file and store as attribute.
# These values can be used when looking for bad settings which are not actually active
# but may become active if other configurations are changed
if main_config_data:
self.config_data = [main_config_data[0]] + config_files_data + [main_config_data[1]]
else:
self.config_data = config_files_data
# Store active settings - the last parsed value us stored
self.data = {}
for _, _, full_data in self.config_data:
copy_data = full_data.copy()
for option, parsed_data in copy_data.items():
if isinstance(parsed_data, dict):
if option not in self.data:
self.data[option] = {}
dict_deep_merge(self.data[option], parsed_data)
else:
if option not in self.data:
self.data[option] = []
self.data[option].extend(parsed_data)
[docs] def get_setting_list(self, directive, section=None):
"""
Returns the parsed data of the specified directive as a list
Parameters:
directive (str): The directive to look for
section (str or tuple): The section the directive belongs to
- str: The section type, e.g. "IfModule"
- tuple(section, section_name): e.g. ("IfModule", "prefork")
Note::
`section_name` can be ignored or can be a part of the actual name.
Returns:
(list of dict or named tuple `ParsedData`):
When `section` is not None, returns the list of dict that wraps
the section and the directive's named tuples ParsedData, in
order how they are parsed.
When `section` is None, returns the list of named tuples
ParsedData, in order how they are parsed.
If directive or section does not exist, returns empty list.
"""
def _deep_search(data, dr, sc):
"""
Utility function to get search the directive `dr` in the nested
dict
Parameters:
data (dict): The target dictionary
dr (str): The directive to look for
sc (tuple): The section the directive belongs to
Returns:
(list of dict): List of dict that wraps the section and the
directive's named tuples ParsedData in order how they are parsed.
"""
result = []
for d, v in data.items():
if isinstance(d, tuple):
if d[0] == sc[0] and sc[1] in d[1]:
val = v.get(dr)
if val:
result.append({d: val})
result.extend(_deep_search(v, dr, sc))
return result
if section:
if isinstance(section, str):
section = (section, '')
elif isinstance(section, tuple) and len(section) == 1:
section = (section[0], '')
elif (not isinstance(section, tuple) or (len(section) == 0 or len(section) > 2)):
return []
return _deep_search(self.data, directive, section)
return self.data.get(directive, [])
[docs] def get_active_setting(self, directive, section=None):
"""
Returns the parsed data of the specified directive as a list of named tuples.
Parameters:
directive (str): The directive to look for
section (str or tuple): The section the directive belongs to
- str: The section type, e.g. "IfModule"
- tuple(section, section_name): e.g. ("IfModule", "prefork")
Note::
`section_name` can be ignored or can be a part of the actual name.
Returns:
(list or named tuple `ParsedData`):
When `section` is not None, returns the list of named tuples
ParsedData, in order how they are parsed.
If directive or section does not exist, returns empty list.
When `section` is None, returns the named tuple ParsedData of
the directive directly.
If directive or section does not exist, returns None.
"""
values_list = self.get_setting_list(directive, section)
if section is not None:
if values_list:
for i, val in enumerate(values_list):
# From each section, preserve only the last ParsedData
# {(section, ""): [ParsedData, ParsedData]} ---> ParsedData
values_list[i] = list(val.values())[0][-1]
return values_list
return []
else:
if values_list:
return values_list[-1]
[docs] def get_section_list(self, section):
"""
Returns the specified sections.
Parameters:
section (str): The section to look for, e.g. "Directory"
Returns:
(list of tuple): List of tuples, each tuple has three elements - the
first being a tuple of the section and section name, the second
being the file name of the file where that section resides,
the third being the full file path of the file. Therefore, the result
looks like this: [(('VirtualHost', '192.0.2.1'), '00-z.conf',
'/etc/httpd/conf.d/00-z.conf')]
If section does not exist, returns empty list.
"""
def _deep_search(data, sc):
"""
Utility function to search for sections in the nested dict
Parameters:
data (dict): The target dictionary
sc (str): The section the directive belongs to
Returns:
(list of tuple): List of tuples, each tuple has three elements - the
first being a tuple of the section and section name, the second
being the file name of the file where that section resides,
the third being the full file path of the file. Therefore, the result
looks like this: [(('VirtualHost', '192.0.2.1'), '00-z.conf',
'/etc/httpd/conf.d/00-z.conf')]
"""
result = []
for d, v in data.items():
if isinstance(d, tuple):
if d[0] == sc:
# file of the section
sect_file_name = None
sect_file_path = None
for subkey, subvalue in v.items():
if subvalue and isinstance(subkey, str) and isinstance(subvalue, list) and isinstance(subvalue[0], ParsedData):
# it is a directive, not a section, there's at least one ParsedData
sect_file_name = subvalue[0].file_name
sect_file_path = subvalue[0].file_path
# assuming all directives in this section come from the same file
break
result.append((d, sect_file_name, sect_file_path))
else:
result.extend(_deep_search(v, sc))
return result
if section:
return _deep_search(self.data, section)
return []
class DocParser(object):
def __init__(self, ctx):
self.ctx = ctx
Complex = Forward()
Comment = (WS >> OneLineComment("#")).map(lambda x: None)
Name = String(string.ascii_letters + "_/")
Num = Number & (WSChar | LineEnd)
StartName = WS >> PosMarker(StartTagName(Letters)) << WS
EndName = WS >> EndTagName(Letters, ignore_case=True) << WS
Cont = Char("\\") + EOL
AttrStart = Many(WSChar)
AttrEnd = (Many(WSChar) + Cont) | Many(WSChar)
OpAttr = (Literal("!=") | Literal("<=") | Literal(">=") | InSet("<>")) & WSChar
BareAttr = String(set(string.printable) - (set(string.whitespace) | set("<>'\"")))
Attr = AttrStart >> (Num | QuotedString | OpAttr | BareAttr) << AttrEnd
Attrs = Many(Attr)
StartTag = (WS + LT) >> (StartName + Attrs) << (GT + WS)
EndTag = (WS + LT + FS) >> EndName << (GT + WS)
Simple = WS >> (Lift(self.to_directive) * PosMarker(Name) * Attrs) << WS
Stanza = Simple | Complex | Comment | Many(WSChar | EOL, lower=1).map(lambda x: None)
Complex <= (Lift(self.to_section) * StartTag * Many(Stanza).map(skip_none)) << EndTag
Doc = Many(Stanza).map(skip_none)
self.Top = Doc + EOF
def typed(self, val):
try:
v = val.lower()
if v in ("on", "yes", "true"):
return True
if v in ("off", "no", "false"):
return False
except:
pass
return val
def to_directive(self, name, attrs):
attrs = attrs if len(attrs) > 1 else [self.typed(a) for a in attrs]
return Directive(name=name.value, attrs=attrs, lineno=name.lineno,
src=self.ctx)
def to_section(self, tag, children):
name, attrs = tag
attrs = attrs if len(attrs) > 1 else [self.typed(a) for a in attrs]
return Section(name=name.value, attrs=attrs, children=children,
lineno=name.lineno, src=self.ctx)
def __call__(self, content):
try:
return self.Top(content)
except:
raise
[docs]def parse_doc(content, ctx=None):
""" Parse a configuration document into a tree that can be queried. """
if isinstance(content, list):
content = "\n".join(content)
parse = DocParser(ctx)
result = parse(content)[0]
return Entry(children=result, src=ctx)
@parser(Specs.httpd_conf, continue_on_error=False)
class _HttpdConf(ConfigParser):
""" Parser for individual httpd configuration files. """
def __init__(self, *args, **kwargs):
self.parse = DocParser(self)
super(_HttpdConf, self).__init__(*args, **kwargs)
def parse_doc(self, content):
if isinstance(content, list):
content = "\n".join(content)
result = self.parse(content)[0]
return Entry(children=result, src=self)
[docs]@combiner(_HttpdConf)
class HttpdConfTree(ConfigCombiner):
"""
Exposes httpd configuration through the parsr query interface. Correctly
handles all include directives.
See the :py:class:`insights.core.ConfigComponent` class for example usage.
"""
def __init__(self, confs):
includes = startswith("Include")
super(HttpdConfTree, self).__init__(confs, "httpd.conf", includes)
@property
def conf_path(self):
res = self.main.find("ServerRoot")
return res.value if res else "/etc/httpd"
@parser(Specs.httpd_conf_scl_httpd24, continue_on_error=False)
class _HttpdConfSclHttpd24(ConfigParser):
""" Parser for individual httpd configuration files. """
def parse_doc(self, content):
return parse_doc(content, ctx=self)
[docs]@combiner(_HttpdConfSclHttpd24)
class HttpdConfSclHttpd24Tree(ConfigCombiner):
"""
Exposes httpd configuration Software Collection httpd24 through the parsr query
interface. Correctly handles all include directives.
See the :py:class:`insights.core.ConfigComponent` class for example usage.
"""
def __init__(self, confs):
includes = startswith("Include")
super(HttpdConfSclHttpd24Tree, self).__init__(confs, "httpd.conf", includes)
@property
def conf_path(self):
res = self.main.find("ServerRoot")
return res.value if res else "/opt/rh/httpd24/root/etc/httpd"
@parser(Specs.httpd_conf_scl_jbcs_httpd24, continue_on_error=False)
class _HttpdConfSclJbcsHttpd24(ConfigParser):
""" Parser for individual httpd configuration files. """
def parse_doc(self, content):
return parse_doc(content, ctx=self)
[docs]@combiner(_HttpdConfSclJbcsHttpd24)
class HttpdConfSclJbcsHttpd24Tree(ConfigCombiner):
"""
Exposes httpd configuration Software Collection jbcs-httpd24 through the parsr query
interface. Correctly handles all include directives.
See the :py:class:`insights.core.ConfigComponent` class for example usage.
"""
def __init__(self, confs):
includes = startswith("Include")
super(HttpdConfSclJbcsHttpd24Tree, self).__init__(confs, "httpd.conf", includes)
@property
def conf_path(self):
res = self.main.find("ServerRoot")
return res.value if res else "/opt/rh/jbcs-httpd24/root/etc/httpd"
[docs]def get_tree(root=None):
"""
This is a helper function to get an httpd configuration component for your
local machine or an archive. Use it in interactive sessions.
"""
return run(HttpdConfTree, root=root).get(HttpdConfTree)
is_private = pred(lambda x: ip_address(six.u(x)).is_private)
"""
Predicate to check if an ip address is private.
Example:
conf["VirtualHost", in_network("128.39.0.0/16")]
"""
in_network = pred2(lambda x, y: (ip_address(six.u(x)) in ip_network(six.u(y))))
"""
Predicate to check if an ip address is in a given network.
Example:
conf["VirtualHost", in_network("128.39.0.0/16")]
"""
if __name__ == "__main__":
run(HttpdConfTree, print_summary=True)