diff options
author | Pavel Moravec <pmoravec@redhat.com> | 2019-08-25 12:20:37 +0200 |
---|---|---|
committer | Pavel Moravec <pmoravec@redhat.com> | 2019-08-25 12:20:37 +0200 |
commit | 1e60189285745480f1cef07ef33bd8a3aab0e1a3 (patch) | |
tree | a8bdd15bdd69f28cd53783c07bb365e874a76362 | |
parent | a5edbabc4ef815fb9c55c90798ceddf2f121aacd (diff) | |
download | sos-1e60189285745480f1cef07ef33bd8a3aab0e1a3.tar.gz |
[sosreport,reporting] replace HTML reports by Report subclass
Current HTML report generation is slow and its implementation is full
of scattered html code. We shall utilize Report class instead.
Additionally, add JSON report format for easy automated parsing.
Further, fixed decoding of unicode content of a report.
Fixed report_tests suite including some pycodestyle errors.
Resolves: #1713
Signed-off-by: Pavel Moravec <pmoravec@redhat.com>
-rw-r--r-- | sos/plugins/__init__.py | 53 | ||||
-rw-r--r-- | sos/reporting.py | 134 | ||||
-rw-r--r-- | sos/sosreport.py | 125 | ||||
-rw-r--r-- | tests/report_tests.py | 73 |
4 files changed, 199 insertions, 186 deletions
diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py index e0d56d91..4e9c4cb4 100644 --- a/sos/plugins/__init__.py +++ b/sos/plugins/__init__.py @@ -1417,59 +1417,6 @@ class Plugin(object): """ pass - def report(self): - """ Present all information that was gathered in an html file that - allows browsing the results. - """ - # make this prettier - html = u'<hr/><a name="%s"></a>\n' % self.name() - - # Intro - html = html + "<h2> Plugin <em>" + self.name() + "</em></h2>\n" - - # Files - if len(self.copied_files): - html = html + "<p>Files copied:<br><ul>\n" - for afile in self.copied_files: - html = html + '<li><a href="%s">%s</a>' % \ - (u'..' + _to_u(afile['dstpath']), _to_u(afile['srcpath'])) - if afile['symlink'] == "yes": - html = html + " (symlink to %s)" % _to_u(afile['pointsto']) - html = html + '</li>\n' - html = html + "</ul></p>\n" - - # Command Output - if len(self.executed_commands): - html = html + "<p>Commands Executed:<br><ul>\n" - # convert file name to relative path from our root - # don't use relpath - these are HTML paths not OS paths. - for cmd in self.executed_commands: - if cmd["file"] and len(cmd["file"]): - cmd_rel_path = u"../" + _to_u(self.commons['cmddir']) \ - + "/" + _to_u(cmd['file']) - html = html + '<li><a href="%s">%s</a></li>\n' % \ - (cmd_rel_path, _to_u(cmd['exe'])) - else: - html = html + '<li>%s</li>\n' % (_to_u(cmd['exe'])) - html = html + "</ul></p>\n" - - # Alerts - if len(self.alerts): - html = html + "<p>Alerts:<br><ul>\n" - for alert in self.alerts: - html = html + '<li>%s</li>\n' % _to_u(alert) - html = html + "</ul></p>\n" - - # Custom Text - if self.custom_text != "": - html = html + "<p>Additional Information:<br>\n" - html = html + _to_u(self.custom_text) + "</p>\n" - - if six.PY2: - return html.encode('utf8') - else: - return html - def check_process_by_name(self, process): """Checks if a named process is found in /proc/[0-9]*/cmdline. Returns either True or False.""" diff --git a/sos/reporting.py b/sos/reporting.py index cb97e473..934aaa2f 100644 --- a/sos/reporting.py +++ b/sos/reporting.py @@ -51,12 +51,17 @@ class Report(Node): self.data[node.name] = node.data +def _decode(s): + """returns a string text for a given unicode/str input""" + return (s if isinstance(s, six.text_type) else s.decode('utf8', 'ignore')) + + class Section(Node): """A section is a container for leaf elements. Sections may be nested inside of Report objects only.""" def __init__(self, name): - self.name = name + self.name = _decode(name) self.data = {} def can_add(self, node): @@ -73,9 +78,9 @@ class Command(Leaf): ADDS_TO = "commands" def __init__(self, name, return_code, href): - self.data = {"name": name, + self.data = {"name": _decode(name), "return_code": return_code, - "href": href} + "href": _decode(href)} class CopiedFile(Leaf): @@ -83,16 +88,17 @@ class CopiedFile(Leaf): ADDS_TO = "copied_files" def __init__(self, name, href): - self.data = {"name": name, - "href": href} + self.data = {"name": _decode(name), + "href": _decode(href)} class CreatedFile(Leaf): ADDS_TO = "created_files" - def __init__(self, name): - self.data = {"name": name} + def __init__(self, name, href): + self.data = {"name": _decode(name), + "href": _decode(href)} class Alert(Leaf): @@ -100,7 +106,7 @@ class Alert(Leaf): ADDS_TO = "alerts" def __init__(self, content): - self.data = content + self.data = _decode(content) class Note(Leaf): @@ -108,7 +114,7 @@ class Note(Leaf): ADDS_TO = "notes" def __init__(self, content): - self.data = content + self.data = _decode(content) def ends_bs(string): @@ -125,32 +131,60 @@ def ends_bs(string): class PlainTextReport(object): """Will generate a plain text report from a top_level Report object""" + HEADER = "" + FOOTER = "" LEAF = " * %(name)s" ALERT = " ! %s" NOTE = " * %s" - DIVIDER = "=" * 72 + PLUGLISTHEADER = "Loaded Plugins:" + PLUGLISTITEM = " {name}" + PLUGLISTSEP = "\n" + PLUGLISTMAXITEMS = 5 + PLUGLISTFOOTER = "" + PLUGINFORMAT = "{name}" + PLUGDIVIDER = "=" * 72 subsections = ( - (Command, LEAF, "- commands executed:"), - (CopiedFile, LEAF, "- files copied:"), - (CreatedFile, LEAF, "- files created:"), - (Alert, ALERT, "- alerts:"), - (Note, NOTE, "- notes:"), + (Command, LEAF, "- commands executed:", ""), + (CopiedFile, LEAF, "- files copied:", ""), + (CreatedFile, LEAF, "- files created:", ""), + (Alert, ALERT, "- alerts:", ""), + (Note, NOTE, "- notes:", ""), ) line_buf = [] def __init__(self, report_node): - self.report_node = report_node + self.report_data = sorted(six.iteritems(report_node.data)) def unicode(self): self.line_buf = line_buf = [] - for section_name, section_contents in sorted(six.iteritems( - self.report_node.data)): - line_buf.append(section_name + "\n" + self.DIVIDER) - for type_, format_, header in self.subsections: + + if (len(self.HEADER) > 0): + line_buf.append(self.HEADER) + + # generate section/plugin list, split long list to multiple lines + line_buf.append(self.PLUGLISTHEADER) + line = "" + i = 0 + plugcount = len(self.report_data) + for section_name, _ in self.report_data: + line += self.PLUGLISTITEM.format(name=section_name) + i += 1 + if (i % self.PLUGLISTMAXITEMS == 0) and (i < plugcount): + line += self.PLUGLISTSEP + line += self.PLUGLISTFOOTER + line_buf.append(line) + + for section_name, section_contents in self.report_data: + line_buf.append(self.PLUGDIVIDER) + line_buf.append(self.PLUGINFORMAT.format(name=section_name)) + for type_, format_, header, footer in self.subsections: self.process_subsection(section_contents, type_.ADDS_TO, - header, format_) + header, format_, footer) + + if (len(self.FOOTER) > 0): + line_buf.append(self.FOOTER) # Workaround python.six mishandling of strings ending in '/' by # adding a single space following any '\' at end-of-line. @@ -158,16 +192,70 @@ class PlainTextReport(object): line_buf = [line + " " if ends_bs(line) else line for line in line_buf] output = u'\n'.join(map(lambda i: (i if isinstance(i, six.text_type) - else six.u(i)), line_buf)) + else i.decode('utf8', 'ignore')), + line_buf)) if six.PY3: return output else: return output.encode('utf8') - def process_subsection(self, section, key, header, format_): + def process_subsection(self, section, key, header, format_, footer): if key in section: self.line_buf.append(header) for item in section.get(key): self.line_buf.append(format_ % item) + if (len(footer) > 0): + self.line_buf.append(footer) + + +class HTMLReport(PlainTextReport): + """Will generate a HTML report from a top_level Report object""" + + HEADER = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> + <head> + <link rel="stylesheet" type="text/css" media="screen" + href="donot.css" /> + <meta http-equiv="Content-Type" content="text/html; + charset=utf-8" /> + <title>Sos System Report</title> + <style> + td { + padding: 0 5px; + } + </style> + </head> + <body>\n""" + FOOTER = "</body></html>" + LEAF = '<li><a href="%(href)s">%(name)s</a></li>' + ALERT = "<li>%s</li>" + NOTE = "<li>%s</li>" + PLUGLISTHEADER = "<h3>Loaded Plugins:</h3><table><tr>" + PLUGLISTITEM = '<td><a href="#{name}">{name}</a></td>\n' + PLUGLISTSEP = "</tr>\n<tr>" + PLUGLISTMAXITEMS = 5 + PLUGLISTFOOTER = "</tr></table>" + PLUGINFORMAT = '<a name="{name}"></a><h2>Plugin <em>{name}</em></h2>' + PLUGDIVIDER = "<hr/>\n" + + subsections = ( + (Command, LEAF, "<p>Commands executed:<br><ul>", "</ul></p>"), + (CopiedFile, LEAF, "<p>Files copied:<br><ul>", "</ul></p>"), + (CreatedFile, LEAF, "<p>Files created:<br><ul>", "</ul></p>"), + (Alert, ALERT, "<p>Alerts:<br><ul>", "</ul></p>"), + (Note, NOTE, "<p>Notes:<br><ul>", "</ul></p>"), + ) + + +class JSONReport(PlainTextReport): + """Will generate a JSON report from a top_level Report object""" + + def unicode(self): + output = json.dumps(self.report_data, indent=4, ensure_ascii=False) + if six.PY3: + return output + else: + return output.encode('utf8') # vim: set et ts=4 sw=4 : diff --git a/sos/sosreport.py b/sos/sosreport.py index ad826ddd..11cae530 100644 --- a/sos/sosreport.py +++ b/sos/sosreport.py @@ -37,7 +37,8 @@ from sos import _arg_defaults, SoSOptions import sos.policies from sos.archive import TarFileArchive from sos.reporting import (Report, Section, Command, CopiedFile, CreatedFile, - Alert, Note, PlainTextReport) + Alert, Note, PlainTextReport, JSONReport, + HTMLReport) # PYCOMPAT import six @@ -1103,9 +1104,10 @@ class SoSReport(object): ]) + '\n' self.archive.add_string(env, 'environment') - def plain_report(self): + def generate_reports(self): report = Report() + # generate report content for plugname, plug in self.loaded_plugins: section = Section(name=plugname) @@ -1121,97 +1123,43 @@ class SoSReport(object): for cmd in plug.executed_commands: section.add(Command(name=cmd['exe'], return_code=0, - href="../" + cmd['file'])) + href=os.path.join( + "..", + self.get_commons()['cmddir'], + cmd['file'] + ))) for content, f in plug.copy_strings: - section.add(CreatedFile(name=f)) + section.add(CreatedFile(name=f, + href=os.path.join( + "..", + "sos_strings", + plugname, + f))) report.add(section) - try: - fd = self.get_temp_file() - output = PlainTextReport(report).unicode() - fd.write(output) - fd.flush() - self.archive.add_file(fd, dest=os.path.join('sos_reports', - 'sos.txt')) - except (OSError, IOError) as e: - if e.errno in fatal_fs_errors: - self.ui_log.error("") - self.ui_log.error(" %s while writing text report" - % e.strerror) - self.ui_log.error("") - self._exit(1) - def html_report(self): - try: - self._html_report() - except (OSError, IOError) as e: - if e.errno in fatal_fs_errors: - self.ui_log.error("") - self.ui_log.error(" %s while writing HTML report" - % e.strerror) - self.ui_log.error("") - self._exit(1) - - def _html_report(self): - # Generate the header for the html output file - rfd = self.get_temp_file() - rfd.write(""" - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> - <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> - <head> - <link rel="stylesheet" type="text/css" media="screen" - href="donot.css" /> - <meta http-equiv="Content-Type" content="text/html; - charset=utf-8" /> - <title>Sos System Report</title> - </head> - <body> - """) - - # Make a pass to gather Alerts and a list of module names - allAlerts = [] - plugNames = [] - for plugname, plug in self.loaded_plugins: - for alert in plug.alerts: - allAlerts.append('<a href="#%s">%s</a>: %s' % (plugname, - plugname, - alert)) - plugNames.append(plugname) - - # Create a table of links to the module info - rfd.write("<hr/><h3>Loaded Plugins:</h3>") - rfd.write("<table><tr>\n") - rr = 0 - for i in range(len(plugNames)): - rfd.write('<td><a href="#%s">%s</a></td>\n' % (plugNames[i], - plugNames[i])) - rr = divmod(i, 4)[1] - if (rr == 3): - rfd.write('</tr>') - if not (rr == 3): - rfd.write('</tr>') - rfd.write('</table>\n') - - rfd.write('<hr/><h3>Alerts:</h3>') - rfd.write('<ul>') - for alert in allAlerts: - rfd.write('<li>%s</li>' % alert) - rfd.write('</ul>') - - # Call the report method for each plugin - for plugname, plug in self.loaded_plugins: + # print it in text, JSON and HTML formats + formatlist = ( + (PlainTextReport, "sos.txt", "text"), + (JSONReport, "sos.json", "JSON"), + (HTMLReport, "sos.html", "HTML") + ) + for class_, filename, type_ in formatlist: try: - html = plug.report() - except Exception: - self.handle_exception() - else: - rfd.write(html) - rfd.write("</body></html>") - rfd.flush() - self.archive.add_file(rfd, dest=os.path.join('sos_reports', - 'sos.html')) + fd = self.get_temp_file() + output = class_(report).unicode() + fd.write(output) + fd.flush() + self.archive.add_file(fd, dest=os.path.join('sos_reports', + filename)) + except (OSError, IOError) as e: + if e.errno in fatal_fs_errors: + self.ui_log.error("") + self.ui_log.error(" %s while writing %s report" + % (e.strerror, type_)) + self.ui_log.error("") + self._exit(1) def postproc(self): for plugname, plug in self.loaded_plugins: @@ -1399,8 +1347,7 @@ class SoSReport(object): if not self.opts.no_env_vars: self.collect_env_vars() if not self.opts.noreport: - self.html_report() - self.plain_report() + self.generate_reports() self.postproc() self.version() return self.final_work() diff --git a/tests/report_tests.py b/tests/report_tests.py index dd390669..e18c4cf4 100644 --- a/tests/report_tests.py +++ b/tests/report_tests.py @@ -8,8 +8,9 @@ try: except ImportError: import simplejson as json -from sos.reporting import Report, Section, Command, CopiedFile, CreatedFile, Alert -from sos.reporting import PlainTextReport +from sos.reporting import (Report, Section, Command, CopiedFile, CreatedFile, + Alert, PlainTextReport) + class ReportTest(unittest.TestCase): @@ -38,22 +39,23 @@ class ReportTest(unittest.TestCase): report.add(section2) expected = json.dumps({"section": {}, - "section2": {},}) + "section2": {}, }) self.assertEquals(expected, str(report)) - def test_deeply_nested(self): report = Report() section = Section(name="section") - command = Command(name="a command", return_code=0, href="does/not/matter") + command = Command(name="a command", return_code=0, + href="does/not/matter") section.add(command) report.add(section) - expected = json.dumps({"section": {"commands": [{"name": "a command", - "return_code": 0, - "href": "does/not/matter"}]}}) + expected = json.dumps({"section": { + "commands": [{"name": "a command", + "return_code": 0, + "href": "does/not/matter"}]}}) self.assertEquals(expected, str(report)) @@ -63,22 +65,37 @@ class TestPlainReport(unittest.TestCase): def setUp(self): self.report = Report() self.section = Section(name="plugin") - self.div = PlainTextReport.DIVIDER + self.div = '\n' + PlainTextReport.PLUGDIVIDER + self.pluglist = "Loaded Plugins:\n{pluglist}" + self.defaultheader = u''.join([ + self.pluglist.format(pluglist=" plugin"), + self.div, + "\nplugin\n" + ]) def test_basic(self): - self.assertEquals("", PlainTextReport(self.report).unicode()) + self.assertEquals(self.pluglist.format(pluglist=""), + PlainTextReport(self.report).unicode()) def test_one_section(self): self.report.add(self.section) - self.assertEquals("plugin\n" + self.div, PlainTextReport(self.report).unicode()) + self.assertEquals(self.defaultheader, + PlainTextReport(self.report).unicode() + '\n') def test_two_sections(self): section1 = Section(name="first") section2 = Section(name="second") self.report.add(section1, section2) - self.assertEquals("first\n" + self.div + "\nsecond\n" + self.div, PlainTextReport(self.report).unicode()) + self.assertEquals(u''.join([ + self.pluglist.format(pluglist=" first second"), + self.div, + "\nfirst", + self.div, + "\nsecond" + ]), + PlainTextReport(self.report).unicode()) def test_command(self): cmd = Command(name="ls -al /foo/bar/baz", @@ -87,32 +104,46 @@ class TestPlainReport(unittest.TestCase): self.section.add(cmd) self.report.add(self.section) - self.assertEquals("plugin\n" + self.div + "\n- commands executed:\n * ls -al /foo/bar/baz", - PlainTextReport(self.report).unicode()) + self.assertEquals(u''.join([ + self.defaultheader, + "- commands executed:\n * ls -al /foo/bar/baz" + ]), + PlainTextReport(self.report).unicode()) def test_copied_file(self): cf = CopiedFile(name="/etc/hosts", href="etc/hosts") self.section.add(cf) self.report.add(self.section) - self.assertEquals("plugin\n" + self.div + "\n- files copied:\n * /etc/hosts", - PlainTextReport(self.report).unicode()) + self.assertEquals(u''.join([ + self.defaultheader, + "- files copied:\n * /etc/hosts" + ]), + PlainTextReport(self.report).unicode()) def test_created_file(self): - crf = CreatedFile(name="sample.txt") + crf = CreatedFile(name="sample.txt", + href="../sos_strings/sample/sample.txt") self.section.add(crf) self.report.add(self.section) - self.assertEquals("plugin\n" + self.div + "\n- files created:\n * sample.txt", - PlainTextReport(self.report).unicode()) + self.assertEquals(u''.join([ + self.defaultheader, + "- files created:\n * sample.txt" + ]), + PlainTextReport(self.report).unicode()) def test_alert(self): alrt = Alert("this is an alert") self.section.add(alrt) self.report.add(self.section) - self.assertEquals("plugin\n" + self.div + "\n- alerts:\n ! this is an alert", - PlainTextReport(self.report).unicode()) + self.assertEquals(u''.join([ + self.defaultheader, + "- alerts:\n ! this is an alert" + ]), + PlainTextReport(self.report).unicode()) + if __name__ == "__main__": unittest.main() |