def email(self, recipients, subject=None, should_email=True, should_message_slack=True, should_attach_log=True, should_escape_chars=True): if not subject: assert isinstance(self.title, str) or self.pipeline is not None, "either title or pipeline must be specified when subject is missing" subject = self.title if self.title else self.pipeline.title smtp = { "host": Config.get("email.smtp.host", None), "port": Config.get("email.smtp.port", None), "user": Config.get("email.smtp.user", None), "pass": Config.get("email.smtp.pass", None) } sender = Address(Config.get("email.sender.name", "undefined"), addr_spec=Config.get("email.sender.address", "*****@*****.**")) # 0. Create attachment with complete log (including DEBUG statements) if should_attach_log is True: self.attachLog() attachments = [] for m in self._messages["attachment"]: smb, file, unc = Filesystem.networkpath(m["text"]) base_path = Filesystem.get_base_path(m["text"], self.pipeline.dir_base) relpath = os.path.relpath(m["text"], base_path) if base_path else None if m["text"].startswith(self.reportDir()): relpath = os.path.relpath(m["text"], self.reportDir()) if not [a for a in attachments if a["unc"] == unc]: attachments.append({ "title": "{}{}".format(relpath, ("/" if os.path.isdir(m["text"]) else "")), "smb": smb, "file": file, "unc": unc, "severity": m["severity"] }) # Determine overall status status = "INFO" for message_type in self._messages: for m in self._messages[message_type]: if m["severity"] == "SUCCESS" and status in ["INFO"]: status = "SUCCESS" elif m["severity"] == "WARN" and status in ["INFO", "SUCCESS"]: status = "WARN" elif m["severity"] == "ERROR": status = "ERROR" try: assert isinstance(smtp, dict), "smtp must be a dict" assert isinstance(sender, Address), "sender must be a Address" assert isinstance(recipients, str) or isinstance(recipients, list) or isinstance(recipients, tuple), "recipients must be a str, list or tuple" assert isinstance(self.title, str) or self.pipeline and isinstance(self.pipeline.title, str), "title or pipeline.title must be a str" if isinstance(recipients, str): recipients = [recipients] elif isinstance(recipients, tuple): recipients = list(recipients) if status == "ERROR": for key in Config.get("administrators", default=[]): if key not in recipients: recipients.append(key) # when testing, only allow e-mail addresses defined in the ALLOWED_EMAIL_ADDRESSES_IN_TEST env var if Config.get("test"): subject = "[test] " + subject filtered_recipients = [] for recipient in recipients: if recipient in Config.get("email.allowed_email_addresses_in_test"): filtered_recipients.append(recipient) recipients = filtered_recipients # 1. join lines with severity SUCCESS/INFO/WARN/ERROR markdown_text = [] for m in self._messages["message"]: if should_escape_chars: text = m['text'].replace("&", "&").replace("<", "<").replace(">", ">") else: text = m['text'] if m['preformatted'] is True: markdown_text.append("<pre>{}</pre>".format(text)) elif m['severity'] != 'DEBUG': markdown_text.append(text) if attachments != [] or should_attach_log is True: markdown_text.append("\n----\n") markdown_text.append("\n# Lenker\n") markdown_text.append("\n<ul style=\"list-style: none;\">") # Pick icon and style for INFO-attachments attachment_styles = { "DEBUG": { "icon": "🗎", "style": "" }, "INFO": { "icon": "🛈", "style": "" }, "SUCCESS": { "icon": "😄", "style": "background-color: #bfffbf;" }, "WARN": { "icon": "😟", "style": "background-color: #ffffbf;" }, "ERROR": { "icon": "ðŸ˜", "style": "background-color: #ffbfbf;" } } for attachment in attachments: # UNC links seems to be preserved when viewed in Outlook. # file: and smb: URIs are disallowed or removed. # So these links will only work in Windows. # If we need this to work cross-platform, we would have # to map the network share paths to a web server so that # the transfers go through http:. This could maybe be mapped # using environment variables. li = "<li>" li += "<span style=\"vertical-align: middle; font-size: 200%;\">" + attachment_styles[attachment["severity"]]["icon"] + "</span> " li += "<span style=\"vertical-align: middle; " + attachment_styles[attachment["severity"]]["style"] + "\">" li += "<a href=\"file:///" + attachment["unc"] + "\">" + attachment["title"] + "</a> " li += "<a href=\"" + attachment["smb"] + "\">" + self.img_string + "=\" alt=\"" + attachment["smb"] + "\"/>" + "</a> " li += "</span>" li += "</li>" markdown_text.append(li) markdown_text.append("</ul>\n") label_string = "" for label in self.pipeline.labels: label_string += "[{}] ".format(label) markdown_text.append("\n[{}] {} [{}] [status:{}]".format(self.pipeline.uid, label_string, self.pipeline.publication_format, status)) markdown_text = "\n".join(markdown_text) # 2. parse string as Markdown and render as HTML if should_escape_chars: markdown_html = markdown.markdown(markdown_text, extensions=['markdown.extensions.fenced_code', 'markdown.extensions.codehilite']) else: markdown_html = markdown_text markdown_html = '''<!DOCTYPE html> <html> <head> <meta charset=\"utf-8\"/> <title>''' + subject.replace("&", "&").replace("<", "<").replace(">", ">") + '''</title> </head> <body> ''' + markdown_html + ''' </body> </html> ''' if not should_email: logging.info("[e-mail] Not sending email") else: # 3. build e-mail msg = EmailMessage() msg['Subject'] = re.sub(r"\s", " ", subject).strip() msg['From'] = sender msg['To'] = Report.emailStringsToAddresses(recipients) msg.set_content(markdown_text) msg.add_alternative(markdown_html, subtype="html") logging.info("[e-mail] E-mail with subject '{}' will be sent to: {}".format(msg['Subject'], ", ".join(recipients))) # 4. send e-mail if smtp["host"] and smtp["port"]: smtp_server = "{}:{}".format(smtp["host"], smtp["port"]) logging.info("[e-mail] SMTP server: {}".format(smtp_server)) with smtplib.SMTP(smtp_server) as s: s.ehlo() # s.starttls() if smtp["user"] and smtp["pass"]: s.login(smtp["user"], smtp["pass"]) else: logging.debug("[e-mail] user/pass not configured") logging.debug("[e-mail] sending…") s.send_message(msg) logging.debug("[e-mail] sending complete.") else: logging.warning("[e-mail] host/port not configured") temp_md_obj = tempfile.NamedTemporaryFile(suffix=".md") temp_html_obj = tempfile.NamedTemporaryFile(suffix=".html") with open(temp_md_obj.name, "w") as f: f.write(markdown_text) logging.debug("[e-mail] markdown: {}".format(temp_md_obj.name)) with open(temp_html_obj.name, "w") as f: f.write(markdown_html) logging.debug("[e-mail] html: {}".format(temp_html_obj.name)) if should_attach_log is True: path_mail = os.path.join(self.reportDir(), "email.html") shutil.copy(temp_html_obj.name, path_mail) self.mailpath = Filesystem.networkpath(path_mail) else: yesterday = datetime.now() - timedelta(1) yesterday = str(yesterday.strftime("%Y-%m-%d")) path_mail = os.path.join(self.pipeline.dir_reports, "logs", "dagsrapporter", yesterday, self.pipeline.uid + ".html") shutil.copy(temp_html_obj.name, path_mail) self.mailpath = Filesystem.networkpath(path_mail) except AssertionError as e: logging.error("[e-mail] " + str(e)) if not should_message_slack: logging.warning("Not sending message to slack") else: # 5. send message to Slack slack_attachments = [] for attachment in attachments: color = None if attachment["severity"] == "SUCCESS": color = "good" elif attachment["severity"] == "WARN": color = "warning" elif attachment["severity"] == "ERROR": color = "danger" slack_attachments.append({ "title_link": attachment["smb"], "title": attachment["title"], "fallback": attachment["title"], "color": color }) Slack.slack(text=subject, attachments=slack_attachments)
def plot(self, uids, name): dot = Digraph(name="Produksjonssystem", format="png") dot.graph_attr["bgcolor"] = "transparent" node_ranks = {} for rank in Directory.dirs_ranked: node_ranks[rank["id"]] = [] # remember edges so that we don't plot them twice edges = {} for uid in uids: pipeline = None for p in self.pipelines: if p[0].uid == uid: pipeline = p break if not pipeline: continue group_pipeline = pipeline[0].get_current_group_pipeline() title = group_pipeline.get_group_title() pipeline_id = group_pipeline.get_group_id() # re.sub(r"[^a-z\d]", "", title.lower()) queue = group_pipeline.get_queue() queue_created = len([book for book in queue if Pipeline.get_main_event(book) == "created"]) if queue else 0 queue_deleted = len([book for book in queue if Pipeline.get_main_event(book) == "deleted"]) if queue else 0 queue_modified = len([book for book in queue if Pipeline.get_main_event(book) == "modified"]) if queue else 0 queue_triggered = len([book for book in queue if Pipeline.get_main_event(book) == "triggered"]) if queue else 0 queue_autotriggered = len([book for book in queue if Pipeline.get_main_event(book) == "autotriggered"]) if queue else 0 queue_string = [] if queue_created: queue_string.append("nye:"+str(queue_created)) if queue_modified: queue_string.append("endret:"+str(queue_modified)) if queue_deleted: queue_string.append("slettet:"+str(queue_deleted)) if queue_triggered: queue_string.append("trigget:"+str(queue_triggered)) if queue_autotriggered: queue_string.append("autotrigget:"+str(queue_autotriggered)) queue_string = ", ".join(queue_string) queue_size = 0 if queue: queue_size = len(queue) if not group_pipeline.should_handle_autotriggered_books(): queue_size -= queue_autotriggered book = Metadata.pipeline_book_shortname(group_pipeline) relpath_in = None netpath_in = "" rank_in = None if pipeline[0].dir_in: for rank in Directory.dirs_ranked: for dir in rank["dirs"]: if os.path.normpath(pipeline[0].dir_in) == os.path.normpath(rank["dirs"][dir]): rank_in = rank["id"] break if pipeline[0].dir_in and not pipeline[0].dir_base: relpath_in = os.path.basename(os.path.dirname(pipeline[0].dir_in)) elif pipeline[0].dir_in and pipeline[0].dir_base: base_path = Filesystem.get_base_path(pipeline[0].dir_in, pipeline[0].dir_base) relpath_in = os.path.relpath(pipeline[0].dir_in, base_path) if "master" in pipeline[0].dir_base and pipeline[0].dir_base["master"] == base_path: pass else: if pipeline[0].dir_in not in self.buffered_network_paths: smb, file, unc = Filesystem.networkpath(pipeline[0].dir_in) host = Filesystem.get_host_from_url(smb) self.buffered_network_paths[pipeline[0].dir_in] = smb self.buffered_network_hosts[pipeline[0].dir_in] = host netpath_in = self.buffered_network_hosts[pipeline[0].dir_in] if not netpath_in: netpath_in = self.buffered_network_paths[pipeline[0].dir_in] book_count_in = self.get_book_count(pipeline[0].dir_in) label_in = "< <font point-size='24'>{}</font>{}{} >".format( relpath_in, "\n<br/><i><font point-size='20'>{} {}</font></i>".format(book_count_in, "bok" if book_count_in == 1 else "bøker"), "\n<br/><i><font point-size='20'>{}</font></i>".format(netpath_in.replace("\\", "\\\\")) if netpath_in else "") relpath_out = None netpath_out = "" rank_out = None if pipeline[0].dir_out: for rank in Directory.dirs_ranked: for dir in rank["dirs"]: if os.path.normpath(pipeline[0].dir_out) == os.path.normpath(rank["dirs"][dir]): rank_out = rank["id"] break if pipeline[0].dir_out and not pipeline[0].dir_base: relpath_out = os.path.basename(os.path.dirname(pipeline[0].dir_out)) elif pipeline[0].dir_out and pipeline[0].dir_base: base_path = Filesystem.get_base_path(pipeline[0].dir_out, pipeline[0].dir_base) relpath_out = os.path.relpath(pipeline[0].dir_out, base_path) if "master" in pipeline[0].dir_base and pipeline[0].dir_base["master"] == base_path: pass else: if pipeline[0].dir_out not in self.buffered_network_paths: smb, file, unc = Filesystem.networkpath(pipeline[0].dir_out) host = Filesystem.get_host_from_url(smb) self.buffered_network_paths[pipeline[0].dir_out] = unc self.buffered_network_hosts[pipeline[0].dir_out] = host netpath_out = self.buffered_network_hosts[pipeline[0].dir_out] if not netpath_out: netpath_out = self.buffered_network_paths[pipeline[0].dir_out] book_count_out = self.get_book_count(pipeline[0].dir_out, pipeline[0].parentdirs) label_out = "< <font point-size='24'>{}</font>{}{} >".format( relpath_out, "\n<br/><i><font point-size='20'>{} {}</font></i>".format(book_count_out, "bok" if book_count_out == 1 else "bøker"), "\n<br/><i><font point-size='20'>{}</font></i>".format(netpath_out.replace("\\", "\\\\")) if netpath_out else "") if rank_out: node_ranks[rank_out].append(pipeline_id) elif rank_in: next_rank = self.next_rank(rank_in) if next_rank: node_ranks[next_rank].append(pipeline_id) else: node_ranks[rank_in].append(pipeline_id) state = group_pipeline.get_state() status = group_pipeline.get_status() progress_text = group_pipeline.get_progress() pipeline_label = "< <font point-size='26'>{}</font>{} >".format( title, "".join(["\n<br/><i><font point-size='22'>{}</font></i>".format(val) for val in [queue_string, progress_text, status] if val])) fillcolor = "lightskyblue1" if book or queue_size: fillcolor = "lightslateblue" elif state == "considering": fillcolor = "lightskyblue3" elif not group_pipeline.running: fillcolor = "white" elif isinstance(group_pipeline, DummyPipeline): fillcolor = "snow" dot.attr("node", shape="box", style="filled", fillcolor=fillcolor) dot.node(pipeline_id, pipeline_label.replace("\\", "\\\\")) if relpath_in: fillcolor = "wheat" if not pipeline[0].dir_in_obj or not pipeline[0].dir_in_obj.is_available(): fillcolor = "white" dot.attr("node", shape="folder", style="filled", fillcolor=fillcolor) dot.node(pipeline[1], label_in) if pipeline[1] not in edges: edges[pipeline[1]] = [] if pipeline_id not in edges[pipeline[1]]: edges[pipeline[1]].append(pipeline_id) dot.edge(pipeline[1], pipeline_id) node_ranks[rank_in].append(pipeline[1]) if relpath_out: fillcolor = "wheat" if not pipeline[0].dir_out_obj or not pipeline[0].dir_out_obj.is_available(): fillcolor = "white" dot.attr("node", shape="folder", style="filled", fillcolor=fillcolor) dot.node(pipeline[2], label_out) if pipeline_id not in edges: edges[pipeline_id] = [] if pipeline[2] not in edges[pipeline_id]: edges[pipeline_id].append(pipeline[2]) dot.edge(pipeline_id, pipeline[2]) node_ranks[rank_out].append(pipeline[2]) for rank in node_ranks: subgraph = Digraph("cluster_" + rank, graph_attr={"style": "dotted"}) subgraph.graph_attr["bgcolor"] = "#FFFFFFAA" if node_ranks[rank]: subgraph.attr("node", shape="none", style="filled", fillcolor="transparent") subgraph.node("_ranklabel_" + rank, "< <i><font point-size='28'>{}</font></i> >".format(" <br/>".join(str(self.rank_name(rank)).split(" ")))) for dir in node_ranks[rank]: subgraph.node(dir) dot.subgraph(subgraph) dot.render(os.path.join(self.report_dir, name + "_")) # there seems to be some race condition when doing this across a mounted network drive, # so if we get an exception we retry a few times and hope that it works. # see: https://github.com/nlbdev/produksjonssystem/issues/81 for t in reversed(range(10)): try: shutil.copyfile(os.path.join(self.report_dir, name + "_.png"), os.path.join(self.report_dir, name + ".png")) with open(os.path.join(self.report_dir, name + ".js"), "w") as javascript_file: javascript_file.write("setTime(\"{}\");".format(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) break except Exception as e: logging.debug(" Unable to copy plot image: {}".format(os.path.join(self.report_dir, name + "_.png"))) time.sleep(0.5) if t == 0: raise e dashboard_file = os.path.join(self.report_dir, name + ".html") if not os.path.isfile(dashboard_file): dashboard_template = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../dashboard.html')) if not os.path.exists(self.report_dir): os.makedirs(self.report_dir) shutil.copyfile(dashboard_template, dashboard_file)