CSIT-1288: Prepare data to be sent by Jenkins
[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 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         self._spec = spec.alerting
74         self._path_failed_tests = spec.environment["paths"]["DIR[STATIC,VPP]"]
75
76         # Verify and validate input specification:
77         self.configs = self._spec.get("configurations", None)
78         if not self.configs:
79             raise AlertingError("No alert configuration is specified.")
80         for config_type, config_data in self.configs.iteritems():
81             if config_type == "email":
82                 if not config_data.get("server", None):
83                     raise AlertingError("Parameter 'server' is missing.")
84                 if not config_data.get("address-to", None):
85                     raise AlertingError("Parameter 'address-to' (recipient) is "
86                                         "missing.")
87                 if not config_data.get("address-from", None):
88                     raise AlertingError("Parameter 'address-from' (sender) is "
89                                         "missing.")
90             elif config_type == "jenkins":
91                 if not isdir(config_data.get("output-dir", "")):
92                     raise AlertingError("Parameter 'output-dir' is "
93                                         "missing or it is not a directory.")
94                 if not config_data.get("output-file", None):
95                     raise AlertingError("Parameter 'output-file' is missing.")
96             else:
97                 raise AlertingError("Alert of type '{0}' is not implemented.".
98                                     format(config_type))
99
100         self.alerts = self._spec.get("alerts", None)
101         if not self.alerts:
102             raise AlertingError("No alert is specified.")
103         for alert, alert_data in self.alerts.iteritems():
104             if not alert_data.get("title", None):
105                 raise AlertingError("Parameter 'title' is missing.")
106             if not alert_data.get("type", None) in self._ALERTS:
107                 raise AlertingError("Parameter 'failed-tests' is missing or "
108                                     "incorrect.")
109             if not alert_data.get("way", None) in self.configs.keys():
110                 raise AlertingError("Parameter 'way' is missing or incorrect.")
111             if not alert_data.get("include", None):
112                 raise AlertingError("Parameter 'include' is missing or the "
113                                     "list is empty.")
114
115     def __str__(self):
116         """Return string with human readable description of the alert.
117
118         :returns: Readable description.
119         :rtype: str
120         """
121         return "configs={configs}, alerts={alerts}".format(
122             configs=self.configs, alerts=self.alerts)
123
124     def __repr__(self):
125         """Return string executable as Python constructor call.
126
127         :returns: Executable constructor call.
128         :rtype: str
129         """
130         return "Alerting(spec={spec})".format(
131             spec=self._spec)
132
133     def generate_alerts(self):
134         """Generate alert(s) using specified way(s).
135         """
136
137         for alert, alert_data in self.alerts.iteritems():
138             if alert_data["way"] == "email":
139                 text, html = self._create_alert_message(alert_data)
140                 conf = self.configs["email"]
141                 self._send_email(server=conf["server"],
142                                  addr_from=conf["address-from"],
143                                  addr_to=conf["address-to"],
144                                  subject=alert_data["title"],
145                                  text=text,
146                                  html=html)
147             elif alert_data["way"] == "jenkins":
148                 self._generate_files_for_jenkins(alert_data)
149             else:
150                 raise AlertingError("Alert with way '{0}' is not implemented.".
151                                     format(alert_data["way"]))
152
153     @staticmethod
154     def _send_email(server, addr_from, addr_to, subject, text=None, html=None):
155         """Send an email using predefined configuration.
156
157         :param server: SMTP server used to send email.
158         :param addr_from: Sender address.
159         :param addr_to: Recipient address(es).
160         :param subject: Subject of the email.
161         :param text: Message in the ASCII text format.
162         :param html: Message in the HTML format.
163         :type server: str
164         :type addr_from: str
165         :type addr_to: list
166         :type subject: str
167         :type text: str
168         :type html: str
169         """
170
171         if not text and not html:
172             raise AlertingError("No text/data to send.")
173
174         msg = MIMEMultipart('alternative')
175         msg['Subject'] = subject
176         msg['From'] = addr_from
177         msg['To'] = ", ".join(addr_to)
178
179         if text:
180             msg.attach(MIMEText(text, 'plain'))
181         if html:
182             msg.attach(MIMEText(html, 'html'))
183
184         smtp_server = None
185         try:
186             logging.info("Trying to send alert '{0}' ...".format(subject))
187             logging.debug("SMTP Server: {0}".format(server))
188             logging.debug("From: {0}".format(addr_from))
189             logging.debug("To: {0}".format(", ".join(addr_to)))
190             logging.debug("Message: {0}".format(msg.as_string()))
191             smtp_server = smtplib.SMTP(server)
192             smtp_server.sendmail(addr_from, addr_to, msg.as_string())
193         except smtplib.SMTPException as err:
194             raise AlertingError("Not possible to send the alert via email.",
195                                 str(err))
196         finally:
197             if smtp_server:
198                 smtp_server.quit()
199
200     def _create_alert_message(self, alert):
201         """Create the message which is used in the generated alert.
202
203         :param alert: Message is created for this alert.
204         :type alert: dict
205         :returns: Message in the ASCII text and HTML format.
206         :rtype: tuple(str, str)
207         """
208
209         if alert["type"] == "failed-tests":
210             text = ""
211             html = "<html><body>"
212             for item in alert["include"]:
213                 file_name = "{path}/{name}".format(
214                     path=self._path_failed_tests, name=item)
215                 try:
216                     with open("{0}.txt".format(file_name), 'r') as txt_file:
217                         text += "{0}:\n\n".format(
218                             item.replace("failed-tests-", ""))
219                         text += txt_file.read() + "\n" * 2
220                 except IOError:
221                     logging.error("Not possible to read the file '{0}.txt'.".
222                                   format(file_name))
223                 try:
224                     with open("{0}.rst".format(file_name), 'r') as rst_file:
225                         html += "<h2>{0}:</h2>".format(
226                             item.replace("failed-tests-", ""))
227                         html += rst_file.readlines()[2].\
228                             replace("../trending", alert.get("url", ""))
229                         html += "<br>" * 3
230                 except IOError:
231                     logging.error("Not possible to read the file '{0}.rst'.".
232                                   format(file_name))
233             html += "</body></html>"
234         else:
235             raise AlertingError("Alert of type '{0}' is not implemented.".
236                                 format(alert["type"]))
237         return text, html
238
239     def _generate_files_for_jenkins(self, alert):
240         """Create the file which is used in the generated alert.
241
242         :param alert: Files are created for this alert.
243         :type alert: dict
244         """
245
246         config = self.configs[alert["way"]]
247
248         if alert["type"] == "failed-tests":
249             text, html = self._create_alert_message(alert)
250             file_name = "{0}/{1}".format(config["output-dir"],
251                                          config["output-file"])
252             logging.info("Writing the file '{0}.txt' ...".format(file_name))
253             try:
254                 with open("{0}.txt".format(file_name), 'w') as txt_file:
255                     txt_file.write(text)
256             except IOError:
257                 logging.error("Not possible to write the file '{0}.txt'.".
258                               format(file_name))
259             logging.info("Writing the file '{0}.html' ...".format(file_name))
260             try:
261                 with open("{0}.html".format(file_name), 'w') as html_file:
262                     html_file.write(html)
263             except IOError:
264                 logging.error("Not possible to write the file '{0}.html'.".
265                               format(file_name))
266
267             zip_file = config.get("zip-output", None)
268             if zip_file:
269                 logging.info("Writing the file '{0}/{1}' ...".
270                              format(config["output-dir"], zip_file))
271                 execute_command("tar czvf {dir}/{zip} --directory={dir} "
272                                 "{input}.txt {input}.html".
273                                 format(dir=config["output-dir"],
274                                        zip=zip_file,
275                                        input=config["output-file"]))
276         else:
277             raise AlertingError("Alert of type '{0}' is not implemented.".
278                                 format(alert["type"]))