X-Git-Url: https://gerrit.fd.io/r/gitweb?a=blobdiff_plain;f=resources%2Ftools%2Fpresentation%2Fgenerator_alerts.py;h=21ffbd23b7331f316f8827c8ddcc788b0fe9cdce;hb=7d1491f4cbeebc7996fc26ee788d529326760516;hp=83dfe2eb17304ba53317dc99901d0a8e79507bf1;hpb=42fe666a56bb77efec0dd08268ee57e6482962d3;p=csit.git diff --git a/resources/tools/presentation/generator_alerts.py b/resources/tools/presentation/generator_alerts.py index 83dfe2eb17..21ffbd23b7 100644 --- a/resources/tools/presentation/generator_alerts.py +++ b/resources/tools/presentation/generator_alerts.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019 Cisco and/or its affiliates. +# Copyright (c) 2023 Cisco and/or its affiliates. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: @@ -11,15 +11,24 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Generator of alerts: +- failed tests +- regressions +- progressions +""" + + import smtplib import logging +import re +from difflib import SequenceMatcher from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from os.path import isdir +from collections import OrderedDict, defaultdict -from utils import execute_command -from errors import PresentationError +from pal_errors import PresentationError class AlertingError(PresentationError): @@ -33,7 +42,7 @@ class AlertingError(PresentationError): - relevant data if there are any collected (optional parameter details). """ - def __init__(self, msg, details='', level="CRITICAL"): + def __init__(self, msg, details=u'', level=u"CRITICAL"): """Sets the exception message and the level. :param msg: Short description of the encountered problem. @@ -47,16 +56,16 @@ class AlertingError(PresentationError): :type level: str """ - super(AlertingError, self).__init__( - "Alerting: {0}".format(msg), details, level) + super(AlertingError, self).__init__(f"Alerting: {msg}", details, level) def __repr__(self): return ( - "AlertingError(msg={msg!r},details={dets!r},level={level!r})". - format(msg=self._msg, dets=self._details, level=self._level)) + f"AlertingError(msg={self._msg!r},details={self._details!r}," + f"level={self._level!r})" + ) -class Alerting(object): +class Alerting: """Class implementing the alerting mechanism. """ @@ -68,55 +77,60 @@ class Alerting(object): """ # Implemented alerts: - self._ALERTS = ("failed-tests", ) + self._implemented_alerts = (u"failed-tests", ) + + self._spec = spec + + self.error_msgs = list() try: - self._spec = spec.alerting + self._spec_alert = spec.alerting except KeyError as err: - raise AlertingError("Alerting is not configured, skipped.", - repr(err), - "WARNING") + raise AlertingError( + u"Alerting is not configured, skipped.", repr(err), u"WARNING" + ) - self._path_failed_tests = spec.environment["paths"]["DIR[STATIC,VPP]"] + self._path_failed_tests = spec.environment[u"paths"][u"DIR[STATIC,VPP]"] # Verify and validate input specification: - self.configs = self._spec.get("configurations", None) + self.configs = self._spec_alert.get(u"configurations", None) if not self.configs: - raise AlertingError("No alert configuration is specified.") - for config_type, config_data in self.configs.iteritems(): - if config_type == "email": - if not config_data.get("server", None): - raise AlertingError("Parameter 'server' is missing.") - if not config_data.get("address-to", None): - raise AlertingError("Parameter 'address-to' (recipient) is " - "missing.") - if not config_data.get("address-from", None): - raise AlertingError("Parameter 'address-from' (sender) is " - "missing.") - elif config_type == "jenkins": - if not isdir(config_data.get("output-dir", "")): - raise AlertingError("Parameter 'output-dir' is " - "missing or it is not a directory.") - if not config_data.get("output-file", None): - raise AlertingError("Parameter 'output-file' is missing.") + raise AlertingError(u"No alert configuration is specified.") + for config_type, config_data in self.configs.items(): + if config_type == u"email": + if not config_data.get(u"server", None): + raise AlertingError(u"Parameter 'server' is missing.") + if not config_data.get(u"address-to", None): + raise AlertingError(u"Parameter 'address-to' (recipient) " + u"is missing.") + if not config_data.get(u"address-from", None): + raise AlertingError(u"Parameter 'address-from' (sender) is " + u"missing.") + elif config_type == u"jenkins": + if not isdir(config_data.get(u"output-dir", u"")): + raise AlertingError(u"Parameter 'output-dir' is " + u"missing or it is not a directory.") + if not config_data.get(u"output-file", None): + raise AlertingError(u"Parameter 'output-file' is missing.") else: - raise AlertingError("Alert of type '{0}' is not implemented.". - format(config_type)) + raise AlertingError( + f"Alert of type {config_type} is not implemented." + ) - self.alerts = self._spec.get("alerts", None) + self.alerts = self._spec_alert.get(u"alerts", None) if not self.alerts: - raise AlertingError("No alert is specified.") - for alert, alert_data in self.alerts.iteritems(): - if not alert_data.get("title", None): - raise AlertingError("Parameter 'title' is missing.") - if not alert_data.get("type", None) in self._ALERTS: - raise AlertingError("Parameter 'failed-tests' is missing or " - "incorrect.") - if not alert_data.get("way", None) in self.configs.keys(): - raise AlertingError("Parameter 'way' is missing or incorrect.") - if not alert_data.get("include", None): - raise AlertingError("Parameter 'include' is missing or the " - "list is empty.") + raise AlertingError(u"No alert is specified.") + for alert_data in self.alerts.values(): + if not alert_data.get(u"title", None): + raise AlertingError(u"Parameter 'title' is missing.") + if not alert_data.get(u"type", None) in self._implemented_alerts: + raise AlertingError(u"Parameter 'failed-tests' is missing or " + u"incorrect.") + if not alert_data.get(u"way", None) in self.configs.keys(): + raise AlertingError(u"Parameter 'way' is missing or incorrect.") + if not alert_data.get(u"include", None): + raise AlertingError(u"Parameter 'include' is missing or the " + u"list is empty.") def __str__(self): """Return string with human readable description of the alert. @@ -124,8 +138,7 @@ class Alerting(object): :returns: Readable description. :rtype: str """ - return "configs={configs}, alerts={alerts}".format( - configs=self.configs, alerts=self.alerts) + return f"configs={self.configs}, alerts={self.alerts}" def __repr__(self): """Return string executable as Python constructor call. @@ -133,28 +146,19 @@ class Alerting(object): :returns: Executable constructor call. :rtype: str """ - return "Alerting(spec={spec})".format( - spec=self._spec) + return f"Alerting(spec={self._spec})" def generate_alerts(self): """Generate alert(s) using specified way(s). """ - for alert, alert_data in self.alerts.iteritems(): - if alert_data["way"] == "email": - text, html = self._create_alert_message(alert_data) - conf = self.configs["email"] - self._send_email(server=conf["server"], - addr_from=conf["address-from"], - addr_to=conf["address-to"], - subject=alert_data["title"], - text=text, - html=html) - elif alert_data["way"] == "jenkins": - self._generate_files_for_jenkins(alert_data) + for alert_data in self.alerts.values(): + if alert_data[u"way"] == u"jenkins": + self._generate_email_body(alert_data) else: - raise AlertingError("Alert with way '{0}' is not implemented.". - format(alert_data["way"])) + raise AlertingError( + f"Alert with way {alert_data[u'way']} is not implemented." + ) @staticmethod def _send_email(server, addr_from, addr_to, subject, text=None, html=None): @@ -175,110 +179,376 @@ class Alerting(object): """ if not text and not html: - raise AlertingError("No text/data to send.") + raise AlertingError(u"No text/data to send.") - msg = MIMEMultipart('alternative') - msg['Subject'] = subject - msg['From'] = addr_from - msg['To'] = ", ".join(addr_to) + msg = MIMEMultipart(u'alternative') + msg[u'Subject'] = subject + msg[u'From'] = addr_from + msg[u'To'] = u", ".join(addr_to) if text: - msg.attach(MIMEText(text, 'plain')) + msg.attach(MIMEText(text, u'plain')) if html: - msg.attach(MIMEText(html, 'html')) + msg.attach(MIMEText(html, u'html')) smtp_server = None try: - logging.info("Trying to send alert '{0}' ...".format(subject)) - logging.debug("SMTP Server: {0}".format(server)) - logging.debug("From: {0}".format(addr_from)) - logging.debug("To: {0}".format(", ".join(addr_to))) - logging.debug("Message: {0}".format(msg.as_string())) + logging.info(f"Trying to send alert {subject} ...") + logging.debug(f"SMTP Server: {server}") + logging.debug(f"From: {addr_from}") + logging.debug(f"To: {u', '.join(addr_to)}") + logging.debug(f"Message: {msg.as_string()}") smtp_server = smtplib.SMTP(server) smtp_server.sendmail(addr_from, addr_to, msg.as_string()) except smtplib.SMTPException as err: - raise AlertingError("Not possible to send the alert via email.", + raise AlertingError(u"Not possible to send the alert via email.", str(err)) finally: if smtp_server: smtp_server.quit() - def _create_alert_message(self, alert): - """Create the message which is used in the generated alert. + def _get_compressed_failed_tests(self, alert, test_set, sort=True): + """Return the dictionary with compressed faild tests. The compression is + done by grouping the tests from the same area but with different NICs, + frame sizes and number of processor cores. + + For example, the failed tests: + 10ge2p1x520-64b-1c-ethip4udp-ip4scale4000-udpsrcscale15-nat44-mrr + 10ge2p1x520-64b-2c-ethip4udp-ip4scale4000-udpsrcscale15-nat44-mrr + 10ge2p1x520-64b-4c-ethip4udp-ip4scale4000-udpsrcscale15-nat44-mrr + 10ge2p1x520-imix-1c-ethip4udp-ip4scale4000-udpsrcscale15-nat44-mrr + 10ge2p1x520-imix-2c-ethip4udp-ip4scale4000-udpsrcscale15-nat44-mrr + 10ge2p1x520-imix-4c-ethip4udp-ip4scale4000-udpsrcscale15-nat44-mrr + + will be represented as: + ethip4udp-ip4scale4000-udpsrcscale15-nat44 \ + (10ge2p1x520, 64b, imix, 1c, 2c, 4c) + + Structure of returned data: + + { + "trimmed_TC_name_1": { + "nics": [], + "framesizes": [], + "cores": [] + } + ... + "trimmed_TC_name_N": { + "nics": [], + "framesizes": [], + "cores": [] + } + } - :param alert: Message is created for this alert. + :param alert: Files are created for this alert. + :param test_set: Specifies which set of tests will be included in the + result. Its name is the same as the name of file with failed tests. + :param sort: If True, the failed tests are sorted alphabetically. + :type alert: dict + :type test_set: str + :type sort: bool + :returns: CSIT build number, VPP version, Number of passed tests, + Number of failed tests, Compressed failed tests. + :rtype: tuple(str, str, int, int, str, OrderedDict) + """ + + directory = self.configs[alert[u"way"]][u"output-dir"] + failed_tests = defaultdict(dict) + file_path = f"{directory}/{test_set}.txt" + version = u"" + try: + with open(file_path, u'r') as f_txt: + for idx, line in enumerate(f_txt): + if idx == 0: + build = line[:-1] + continue + if idx == 1: + version = line[:-1] + continue + if idx == 2: + passed = line[:-1] + continue + if idx == 3: + failed = line[:-1] + continue + if idx == 4: + minutes = int(line[:-1]) // 60000 + duration = f"{(minutes // 60):02d}:{(minutes % 60):02d}" + continue + try: + line, error_msg = line[:-1].split(u'###', maxsplit=1) + test = line.split(u'-') + name = u'-'.join(test[3:-1]) + if len(error_msg) > 128: + if u";" in error_msg[128:256]: + error_msg = \ + f"{error_msg[:128]}" \ + f"{error_msg[128:].split(u';', 1)[0]}..." + elif u":" in error_msg[128:256]: + error_msg = \ + f"{error_msg[:128]}" \ + f"{error_msg[128:].split(u':', 1)[0]}..." + elif u"." in error_msg[128:256]: + error_msg = \ + f"{error_msg[:128]}" \ + f"{error_msg[128:].split(u'.', 1)[0]}..." + elif u"?" in error_msg[128:256]: + error_msg = \ + f"{error_msg[:128]}" \ + f"{error_msg[128:].split(u'?', 1)[0]}..." + elif u"!" in error_msg[128:256]: + error_msg = \ + f"{error_msg[:128]}" \ + f"{error_msg[128:].split(u'!', 1)[0]}..." + elif u"," in error_msg[128:256]: + error_msg = \ + f"{error_msg[:128]}" \ + f"{error_msg[128:].split(u',', 1)[0]}..." + elif u" " in error_msg[128:256]: + error_msg = \ + f"{error_msg[:128]}" \ + f"{error_msg[128:].split(u' ', 1)[0]}..." + else: + error_msg = error_msg[:128] + + except ValueError: + continue + + for e_msg in self.error_msgs: + if SequenceMatcher(None, e_msg, + error_msg).ratio() > 0.5: + error_msg = e_msg + break + if error_msg not in self.error_msgs: + self.error_msgs.append(error_msg) + + error_msg_index = self.error_msgs.index(error_msg) + + if failed_tests.get(name, {}).get(error_msg_index) is None: + failed_tests[name][error_msg_index] = \ + dict(nics=list(), + framesizes=list(), + cores=list()) + + if test[0] not in \ + failed_tests[name][error_msg_index][u"nics"]: + failed_tests[name][error_msg_index][u"nics"].\ + append(test[0]) + if test[1] not in \ + failed_tests[name][error_msg_index][u"framesizes"]: + failed_tests[name][error_msg_index][u"framesizes"].\ + append(test[1]) + check_core = test[2] + f"[{str(error_msg_index)}]" + if check_core not in \ + failed_tests[name][error_msg_index][u"cores"]: + failed_tests[name][error_msg_index][u"cores"].\ + append(test[2] + "[" + str(error_msg_index) + "]") + + except IOError: + logging.error(f"No such file or directory: {file_path}") + return None, None, None, None, None, None + if sort: + sorted_failed_tests = OrderedDict() + for key in sorted(failed_tests.keys()): + sorted_failed_tests[key] = failed_tests[key] + return build, version, passed, failed, duration, sorted_failed_tests + + return build, version, passed, failed, duration, failed_tests + + def _list_gressions(self, alert, idx, header, re_pro): + """Create a file with regressions or progressions for the test set + specified by idx. + + :param alert: Files are created for this alert. + :param idx: Index of the test set as it is specified in the + specification file. + :param header: The header of the list of [re|pro]gressions. + :param re_pro: 'regressions' or 'progressions'. :type alert: dict - :returns: Message in the ASCII text and HTML format. - :rtype: tuple(str, str) + :type idx: int + :type header: str + :type re_pro: str """ - if alert["type"] == "failed-tests": - text = "" - html = "
" - for item in alert["include"]: - file_name = "{path}/{name}".format( - path=self._path_failed_tests, name=item) - try: - with open("{0}.txt".format(file_name), 'r') as txt_file: - text += "{0}:\n\n".format( - item.replace("failed-tests-", "")) - text += txt_file.read() + "\n" * 2 - except IOError: - logging.error("Not possible to read the file '{0}.txt'.". - format(file_name)) - try: - with open("{0}.rst".format(file_name), 'r') as rst_file: - html += "