Skip to content

Commit

Permalink
fix: Move _LogRotateConf parser out of combiner (#3389)
Browse files Browse the repository at this point in the history
* Due to an issue found with the httpdconf parser that was in
  the combiner I'm moving the newer logrotate parser out of it's
  combiner as well.
* Added docstring to the moved parser, and added doctest.

Signed-off-by: Ryan Blakley <[email protected]>
  • Loading branch information
ryan-blakley authored Apr 19, 2022
1 parent 5135da0 commit 18ef70e
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 88 deletions.
83 changes: 5 additions & 78 deletions insights/combiners/logrotate_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,14 @@
options, and all other options (if there are) will be discarded.
"""

import operator
import os
import string

from fnmatch import fnmatch
from insights.core import ConfigCombiner, ConfigParser
from insights.core.plugins import combiner, parser
from insights.parsers.logrotate_conf import LogrotateConf
from insights.core import ConfigCombiner
from insights.core.plugins import combiner
from insights.parsers.logrotate_conf import LogrotateConf, LogRotateConfPEG
from insights.parsr.query import eq
from insights.specs import Specs
from insights.parsr import (AnyChar, Choice, EOF, EOL, Forward, LeftCurly,
LineEnd, Literal, Many, Number, OneLineComment, Opt, PosMarker,
QuotedString, RightCurly, skip_none, String, WS, WSChar)
from insights.parsr.query import Directive, Entry, Section


@combiner(LogrotateConf)
Expand Down Expand Up @@ -146,65 +140,7 @@ def configfile_of_logfile(self, log_file):
return f


class DocParser(object):
def __init__(self, ctx):
self.ctx = ctx

scripts = set("postrotate prerotate firstaction lastaction preremove".split())
Stanza = Forward()
Spaces = Many(WSChar)
Bare = String(set(string.printable) - (set(string.whitespace) | set("#{}'\"")))
Num = Number & (WSChar | LineEnd)
Comment = OneLineComment("#").map(lambda x: None)
ScriptStart = WS >> PosMarker(Choice([Literal(s) for s in scripts])) << WS
ScriptEnd = Literal("endscript")
Line = (WS >> AnyChar.until(EOL) << WS).map(lambda x: "".join(x))
Lines = Line.until(ScriptEnd | EOF).map(lambda x: "\n".join(x))
Script = ScriptStart + Lines << Opt(ScriptEnd)
Script = Script.map(lambda x: [x[0], [x[1]], None])
BeginBlock = WS >> LeftCurly << WS
EndBlock = WS >> RightCurly
First = PosMarker((Bare | QuotedString)) << Spaces
Attr = Spaces >> (Num | Bare | QuotedString) << Spaces
Rest = Many(Attr)
Block = BeginBlock >> Many(Stanza).map(skip_none).map(self.to_entries) << EndBlock
Stmt = WS >> (Script | (First + Rest + Opt(Block))) << WS
Stanza <= WS >> (Stmt | Comment) << WS
Doc = Many(Stanza).map(skip_none).map(self.to_entries)

self.Top = Doc << EOF

def to_entries(self, x):
ret = []
for i in x:
name, attrs, body = i
if body:
for n in [name.value] + attrs:
ret.append(Section(name=n, children=body, lineno=name.lineno))
else:
ret.append(Directive(name=name.value, attrs=attrs, lineno=name.lineno))
return ret

def __call__(self, content):
return self.Top(content)


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)
return Entry(children=result, src=ctx)


@parser(Specs.logrotate_conf, continue_on_error=False)
class _LogRotateConf(ConfigParser):
def parse_doc(self, content):
return parse_doc("\n".join(content), ctx=self)


@combiner(_LogRotateConf)
@combiner(LogRotateConfPEG)
class LogRotateConfTree(ConfigCombiner):
"""
Exposes logrotate configuration through the parsr query interface.
Expand All @@ -228,12 +164,3 @@ def find_matches(self, confs, pattern):
elif fnmatch(c.file_path, pattern):
results.append(c)
return sorted(results, key=operator.attrgetter("file_name"))


def get_tree(root=None):
"""
This is a helper function to get a logrotate configuration component for
your local machine or an archive. It's for use in interactive sessions.
"""
from insights import run
return run(LogRotateConfTree, root=root).get(LogRotateConfTree)
10 changes: 5 additions & 5 deletions insights/combiners/tests/test_logrotate_conf_tree.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# coding=utf-8
import pytest

from insights.combiners.logrotate_conf import LogRotateConfTree
from insights.parsers import logrotate_conf
from insights.parsr.query import first
from insights.combiners.logrotate_conf import _LogRotateConf, LogRotateConfTree
from insights.tests import context_wrap


Expand Down Expand Up @@ -85,7 +86,7 @@


def test_logrotate_tree():
p = _LogRotateConf(context_wrap(CONF, path="/etc/logrotate.conf"))
p = logrotate_conf.LogRotateConfPEG(context_wrap(CONF, path="/etc/logrotate.conf"))
conf = LogRotateConfTree([p])
assert len(conf["weekly"]) == 1
assert len(conf["/var/log/wtmp"]["missingok"]) == 1
Expand All @@ -97,12 +98,11 @@ def test_logrotate_tree():


def test_junk_space():
p = _LogRotateConf(context_wrap(JUNK_SPACE, path="/etc/logrotate.conf"))
p = logrotate_conf.LogRotateConfPEG(context_wrap(JUNK_SPACE, path="/etc/logrotate.conf"))
conf = LogRotateConfTree([p])
assert "compress" in conf["/var/log/spooler"]


def test_logrotate_conf_combiner_missing_endscript():
with pytest.raises(Exception):
p = _LogRotateConf(context_wrap(LOGROTATE_MISSING_ENDSCRIPT, path='/etc/logrotate.conf')),
print(p)
logrotate_conf.LogRotateConfPEG(context_wrap(LOGROTATE_MISSING_ENDSCRIPT, path='/etc/logrotate.conf')),
118 changes: 115 additions & 3 deletions insights/parsers/logrotate_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@
- ``/etc/logrotate.d/*``
See: http://www.linuxmanpages.org/8/logrotate
"""
import string


from .. import parser, Parser, get_active_lines, LegacyItemAccess
from insights.core import ConfigParser, LegacyItemAccess, Parser
from insights.core.plugins import parser
from insights.parsers import ParseException, get_active_lines
from insights.parsr import (AnyChar, Choice, EOF, EOL, Forward, LeftCurly, LineEnd,
Literal, Many, Number, OneLineComment, Opt, PosMarker,
QuotedString, RightCurly, skip_none, String, WS, WSChar)
from insights.parsr.query import Directive, Entry, Section
from insights.specs import Specs

PAIRED_OPTS = (
Expand Down Expand Up @@ -143,3 +148,110 @@ def _parse_opts(line):
# common options in log_file section
key, val = _parse_opts(line)
log_opts[key] = val


class DocParser(object):
def __init__(self, ctx):
self.ctx = ctx

scripts = set("postrotate prerotate firstaction lastaction preremove".split())
Stanza = Forward()
Spaces = Many(WSChar)
Bare = String(set(string.printable) - (set(string.whitespace) | set("#{}'\"")))
Num = Number & (WSChar | LineEnd)
Comment = OneLineComment("#").map(lambda x: None)
ScriptStart = WS >> PosMarker(Choice([Literal(s) for s in scripts])) << WS
ScriptEnd = Literal("endscript")
Line = (WS >> AnyChar.until(EOL) << WS).map(lambda x: "".join(x))
Lines = Line.until(ScriptEnd | EOF).map(lambda x: "\n".join(x))
Script = ScriptStart + Lines << Opt(ScriptEnd)
Script = Script.map(lambda x: [x[0], [x[1]], None])
BeginBlock = WS >> LeftCurly << WS
EndBlock = WS >> RightCurly
First = PosMarker((Bare | QuotedString)) << Spaces
Attr = Spaces >> (Num | Bare | QuotedString) << Spaces
Rest = Many(Attr)
Block = BeginBlock >> Many(Stanza).map(skip_none).map(self.to_entries) << EndBlock
Stmt = WS >> (Script | (First + Rest + Opt(Block))) << WS
Stanza <= WS >> (Stmt | Comment) << WS
Doc = Many(Stanza).map(skip_none).map(self.to_entries)

self.Top = Doc << EOF

def to_entries(self, x):
ret = []
for i in x:
name, attrs, body = i
if body:
for n in [name.value] + attrs:
ret.append(Section(name=n, children=body, lineno=name.lineno))
else:
ret.append(Directive(name=name.value, attrs=attrs, lineno=name.lineno))
return ret

def __call__(self, content):
try:
return self.Top(content)
except Exception:
raise ParseException("There was an exception when parsing the logrotate config file.")


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)
return Entry(children=result, src=ctx)


@parser(Specs.logrotate_conf)
class LogRotateConfPEG(ConfigParser):
"""
Sample logrotate configuration file::
# sample file
compress
/var/log/messages {
rotate 5
weekly
postrotate
/sbin/killall -HUP syslogd
endscript
}
"/var/log/httpd/access.log" /var/log/httpd/error.log {
rotate 5
mail [email protected]
size=100k
sharedscripts
postrotate
/sbin/killall -HUP httpd
endscript
}
/var/log/news/news.crit
/var/log/news/olds.crit {
monthly
rotate 2
olddir /var/log/news/old
missingok
postrotate
kill -HUP `cat /var/run/inn.pid`
endscript
nocompress
}
Examples:
>>> type(log_rt_peg)
<class 'insights.parsers.logrotate_conf.LogRotateConfPEG'>
>>> 'compress' in log_rt_peg
True
>>> 'weekly' in log_rt['/var/log/messages']
True
>>> log_rt_peg['/var/log/messages']['postrotate'][-1].value
'/sbin/killall -HUP syslogd'
"""
def parse_doc(self, content):
return parse_doc("\n".join(content), ctx=self)
5 changes: 3 additions & 2 deletions insights/parsers/tests/test_logrotate_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@

def test_web_xml_doc_examples():
env = {
'log_rt': LogrotateConf(context_wrap(LOGROTATE_MAN_PAGE_DOC, path='/etc/logrotate.conf')),
}
'log_rt': logrotate_conf.LogrotateConf(context_wrap(LOGROTATE_MAN_PAGE_DOC, path='/etc/logrotate.conf')),
'log_rt_peg': logrotate_conf.LogRotateConfPEG(context_wrap(LOGROTATE_MAN_PAGE_DOC, path='/etc/logrotate.conf')),
}
failed, total = doctest.testmod(logrotate_conf, globs=env)
assert failed == 0

Expand Down

0 comments on commit 18ef70e

Please sign in to comment.