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