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:
6 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 from email.mime.text import MIMEText
18 from email.mime.multipart import MIMEMultipart
19 from os.path import isdir
21 from utils import execute_command
22 from errors import PresentationError
25 class AlertingError(PresentationError):
26 """Exception(s) raised by the alerting module.
28 When raising this exception, put this information to the message in this
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).
36 def __init__(self, msg, details='', level="CRITICAL"):
37 """Sets the exception message and the level.
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".
50 super(AlertingError, self).__init__(
51 "Alerting: {0}".format(msg), details, level)
55 "AlertingError(msg={msg!r},details={dets!r},level={level!r})".
56 format(msg=self._msg, dets=self._details, level=self._level))
59 class Alerting(object):
60 """Class implementing the alerting mechanism.
63 def __init__(self, spec):
66 :param spec: The CPTA specification.
67 :type spec: Specification
71 self._ALERTS = ("failed-tests", )
74 self._spec = spec.alerting
75 except KeyError as err:
76 raise AlertingError("Alerting is not configured, skipped.",
80 self._path_failed_tests = spec.environment["paths"]["DIR[STATIC,VPP]"]
82 # Verify and validate input specification:
83 self.configs = self._spec.get("configurations", None)
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 "
93 if not config_data.get("address-from", None):
94 raise AlertingError("Parameter 'address-from' (sender) is "
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.")
103 raise AlertingError("Alert of type '{0}' is not implemented.".
106 self.alerts = self._spec.get("alerts", None)
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 "
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 "
122 """Return string with human readable description of the alert.
124 :returns: Readable description.
127 return "configs={configs}, alerts={alerts}".format(
128 configs=self.configs, alerts=self.alerts)
131 """Return string executable as Python constructor call.
133 :returns: Executable constructor call.
136 return "Alerting(spec={spec})".format(
139 def generate_alerts(self):
140 """Generate alert(s) using specified way(s).
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"],
153 elif alert_data["way"] == "jenkins":
154 self._generate_files_for_jenkins(alert_data)
156 raise AlertingError("Alert with way '{0}' is not implemented.".
157 format(alert_data["way"]))
160 def _send_email(server, addr_from, addr_to, subject, text=None, html=None):
161 """Send an email using predefined configuration.
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.
177 if not text and not html:
178 raise AlertingError("No text/data to send.")
180 msg = MIMEMultipart('alternative')
181 msg['Subject'] = subject
182 msg['From'] = addr_from
183 msg['To'] = ", ".join(addr_to)
186 msg.attach(MIMEText(text, 'plain'))
188 msg.attach(MIMEText(html, 'html'))
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.",
206 def _create_alert_message(self, alert):
207 """Create the message which is used in the generated alert.
209 :param alert: Message is created for this alert.
211 :returns: Message in the ASCII text and HTML format.
212 :rtype: tuple(str, str)
215 if alert["type"] == "failed-tests":
217 html = "<html><body>"
218 for item in alert["include"]:
219 file_name = "{path}/{name}".format(
220 path=self._path_failed_tests, name=item)
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
227 logging.error("Not possible to read the file '{0}.txt'.".
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", ""))
237 logging.error("Not possible to read the file '{0}.rst'.".
239 html += "</body></html>"
241 raise AlertingError("Alert of type '{0}' is not implemented.".
242 format(alert["type"]))
245 def _generate_files_for_jenkins(self, alert):
246 """Create the file which is used in the generated alert.
248 :param alert: Files are created for this alert.
252 config = self.configs[alert["way"]]
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))
260 with open("{0}.txt".format(file_name), 'w') as txt_file:
263 logging.error("Not possible to write the file '{0}.txt'.".
265 logging.info("Writing the file '{0}.html' ...".format(file_name))
267 with open("{0}.html".format(file_name), 'w') as html_file:
268 html_file.write(html)
270 logging.error("Not possible to write the file '{0}.html'.".
273 zip_file = config.get("zip-output", None)
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"],
281 input=config["output-file"]))
283 raise AlertingError("Alert of type '{0}' is not implemented.".
284 format(alert["type"]))