Add dot1q-ip6base-[ndrpdr|mrr] perf tests for 2-node topology
[csit.git] / resources / tools / presentation / generator_alerts.py
1 # Copyright (c) 2018 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 import smtplib
15 import logging
16
17 from email.mime.text import MIMEText
18 from email.mime.multipart import MIMEMultipart
19 from os.path import isdir
20
21 from errors import PresentationError
22
23
24 class AlertingError(PresentationError):
25     """Exception(s) raised by the alerting module.
26
27     When raising this exception, put this information to the message in this
28     order:
29      - short description of the encountered problem (parameter msg),
30      - relevant messages if there are any collected, e.g., from caught
31        exception (optional parameter details),
32      - relevant data if there are any collected (optional parameter details).
33     """
34
35     def __init__(self, msg, details='', level="CRITICAL"):
36         """Sets the exception message and the level.
37
38         :param msg: Short description of the encountered problem.
39         :param details: Relevant messages if there are any collected, e.g.,
40             from caught exception (optional parameter details), or relevant data
41             if there are any collected (optional parameter details).
42         :param level: Level of the error, possible choices are: "DEBUG", "INFO",
43             "WARNING", "ERROR" and "CRITICAL".
44         :type msg: str
45         :type details: str
46         :type level: str
47         """
48
49         super(AlertingError, self).__init__(
50             "Alerting: {0}".format(msg), details, level)
51
52     def __repr__(self):
53         return (
54             "AlertingError(msg={msg!r},details={dets!r},level={level!r})".
55             format(msg=self._msg, dets=self._details, level=self._level))
56
57
58 class Alerting(object):
59     """Class implementing the alerting mechanism.
60     """
61
62     def __init__(self, spec):
63         """Initialization.
64
65         :param spec: The CPTA specification.
66         :type spec: Specification
67         """
68
69         # Implemented alerts:
70         self._ALERTS = ("failed-tests", )
71
72         self._spec = spec.alerting
73         self._path_failed_tests = spec.environment["paths"]["DIR[STATIC,VPP]"]
74
75         # Verify and validate input specification:
76         self.configs = self._spec.get("configurations", None)
77         if not self.configs:
78             raise AlertingError("No alert configuration is specified.")
79         for config_type, config_data in self.configs.iteritems():
80             if config_type == "email":
81                 if not config_data.get("server", None):
82                     raise AlertingError("Parameter 'server' is missing.")
83                 if not config_data.get("address-to", None):
84                     raise AlertingError("Parameter 'address-to' (recipient) is "
85                                         "missing.")
86                 if not config_data.get("address-from", None):
87                     raise AlertingError("Parameter 'address-from' (sender) is "
88                                         "missing.")
89             elif config_type == "jenkins":
90                 if not isdir(config_data.get("output-dir", "")):
91                     raise AlertingError("Parameter 'output-dir' is "
92                                         "missing or it is not a directory.")
93                 if not config_data.get("output-file", None):
94                     raise AlertingError("Parameter 'output-file' is missing.")
95             else:
96                 raise AlertingError("Alert of type '{0}' is not implemented.".
97                                     format(config_type))
98
99         self.alerts = self._spec.get("alerts", None)
100         if not self.alerts:
101             raise AlertingError("No alert is specified.")
102         for alert, alert_data in self.alerts.iteritems():
103             if not alert_data.get("title", None):
104                 raise AlertingError("Parameter 'title' is missing.")
105             if not alert_data.get("type", None) in self._ALERTS:
106                 raise AlertingError("Parameter 'failed-tests' is missing or "
107                                     "incorrect.")
108             if not alert_data.get("way", None) in self.configs.keys():
109                 raise AlertingError("Parameter 'way' is missing or incorrect.")
110             if not alert_data.get("include", None):
111                 raise AlertingError("Parameter 'include' is missing or the "
112                                     "list is empty.")
113
114     def __str__(self):
115         """Return string with human readable description of the alert.
116
117         :returns: Readable description.
118         :rtype: str
119         """
120         return "configs={configs}, alerts={alerts}".format(
121             configs=self.configs, alerts=self.alerts)
122
123     def __repr__(self):
124         """Return string executable as Python constructor call.
125
126         :returns: Executable constructor call.
127         :rtype: str
128         """
129         return "Alerting(spec={spec})".format(
130             spec=self._spec)
131
132     def generate_alerts(self):
133         """Generate alert(s) using specified way(s).
134         """
135
136         for alert, alert_data in self.alerts.iteritems():
137             if alert_data["way"] == "email":
138                 text, html = self._create_alert_message(alert_data)
139                 conf = self.configs["email"]
140                 self._send_email(server=conf["server"],
141                                  addr_from=conf["address-from"],
142                                  addr_to=conf["address-to"],
143                                  subject=alert_data["title"],
144                                  text=text,
145                                  html=html)
146             elif alert_data["way"] == "jenkins":
147                 self._generate_files_for_jenkins(alert_data)
148             else:
149                 raise AlertingError("Alert with way '{0}' is not implemented.".
150                                     format(alert_data["way"]))
151
152     @staticmethod
153     def _send_email(server, addr_from, addr_to, subject, text=None, html=None):
154         """Send an email using predefined configuration.
155
156         :param server: SMTP server used to send email.
157         :param addr_from: Sender address.
158         :param addr_to: Recipient address(es).
159         :param subject: Subject of the email.
160         :param text: Message in the ASCII text format.
161         :param html: Message in the HTML format.
162         :type server: str
163         :type addr_from: str
164         :type addr_to: list
165         :type subject: str
166         :type text: str
167         :type html: str
168         """
169
170         if not text and not html:
171             raise AlertingError("No text/data to send.")
172
173         msg = MIMEMultipart('alternative')
174         msg['Subject'] = subject
175         msg['From'] = addr_from
176         msg['To'] = ", ".join(addr_to)
177
178         if text:
179             msg.attach(MIMEText(text, 'plain'))
180         if html:
181             msg.attach(MIMEText(html, 'html'))
182
183         smtp_server = None
184         try:
185             logging.info("Trying to send alert '{0}' ...".format(subject))
186             logging.debug("SMTP Server: {0}".format(server))
187             logging.debug("From: {0}".format(addr_from))
188             logging.debug("To: {0}".format(", ".join(addr_to)))
189             logging.debug("Message: {0}".format(msg.as_string()))
190             smtp_server = smtplib.SMTP(server)
191             smtp_server.sendmail(addr_from, addr_to, msg.as_string())
192         except smtplib.SMTPException as err:
193             raise AlertingError("Not possible to send the alert via email.",
194                                 str(err))
195         finally:
196             if smtp_server:
197                 smtp_server.quit()
198
199     def _create_alert_message(self, alert):
200         """Create the message which is used in the generated alert.
201
202         :param alert: Message is created for this alert.
203         :type alert: dict
204         :returns: Message in the ASCII text and HTML format.
205         :rtype: tuple(str, str)
206         """
207
208         if alert["type"] == "failed-tests":
209             text = ""
210             html = "<html><body>"
211             for item in alert["include"]:
212                 file_name = "{path}/{name}".format(
213                     path=self._path_failed_tests, name=item)
214                 try:
215                     with open("{0}.txt".format(file_name), 'r') as txt_file:
216                         text += "{0}:\n\n".format(
217                             item.replace("failed-tests-", ""))
218                         text += txt_file.read() + "\n" * 2
219                 except IOError:
220                     logging.error("Not possible to read the file '{0}.txt'.".
221                                   format(file_name))
222                 try:
223                     with open("{0}.rst".format(file_name), 'r') as rst_file:
224                         html += "<h2>{0}:</h2>".format(
225                             item.replace("failed-tests-", ""))
226                         html += rst_file.readlines()[2].\
227                             replace("../trending", alert.get("url", ""))
228                         html += "<br>" * 3
229                 except IOError:
230                     logging.error("Not possible to read the file '{0}.rst'.".
231                                   format(file_name))
232             html += "</body></html>"
233         else:
234             raise AlertingError("Alert of type '{0}' is not implemented.".
235                                 format(alert["type"]))
236         return text, html
237
238     def _generate_files_for_jenkins(self, alert):
239         """Create the file which is used in the generated alert.
240
241         :param alert: Files are created for this alert.
242         :type alert: dict
243         """
244
245         config = self.configs[alert["way"]]
246
247         if alert["type"] == "failed-tests":
248             text, html = self._create_alert_message(alert)
249             file_name = "{0}/{1}".format(config["output-dir"],
250                                          config["output-file"])
251             logging.info("Writing the file '{0}.txt' ...".format(file_name))
252             try:
253                 with open("{0}.txt".format(file_name), 'w') as txt_file:
254                     txt_file.write(text)
255             except IOError:
256                 logging.error("Not possible to write the file '{0}.txt'.".
257                               format(file_name))
258             logging.info("Writing the file '{0}.html' ...".format(file_name))
259             try:
260                 with open("{0}.html".format(file_name), 'w') as html_file:
261                     html_file.write(html)
262             except IOError:
263                 logging.error("Not possible to write the file '{0}.html'.".
264                               format(file_name))
265         else:
266             raise AlertingError("Alert of type '{0}' is not implemented.".
267                                 format(alert["type"]))