| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 1 | #!/usr/bin/python3 | 
 | 2 | # | 
 | 3 | # Send build performance test report emails | 
 | 4 | # | 
 | 5 | # Copyright (c) 2017, Intel Corporation. | 
 | 6 | # | 
 | 7 | # This program is free software; you can redistribute it and/or modify it | 
 | 8 | # under the terms and conditions of the GNU General Public License, | 
 | 9 | # version 2, as published by the Free Software Foundation. | 
 | 10 | # | 
 | 11 | # This program is distributed in the hope it will be useful, but WITHOUT | 
 | 12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | 
 | 13 | # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | 
 | 14 | # more details. | 
 | 15 | # | 
 | 16 | import argparse | 
 | 17 | import base64 | 
 | 18 | import logging | 
 | 19 | import os | 
 | 20 | import pwd | 
 | 21 | import re | 
 | 22 | import shutil | 
 | 23 | import smtplib | 
 | 24 | import socket | 
 | 25 | import subprocess | 
 | 26 | import sys | 
 | 27 | import tempfile | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 28 | from email.mime.image import MIMEImage | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 29 | from email.mime.multipart import MIMEMultipart | 
 | 30 | from email.mime.text import MIMEText | 
 | 31 |  | 
 | 32 |  | 
 | 33 | # Setup logging | 
 | 34 | logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") | 
 | 35 | log = logging.getLogger('oe-build-perf-report') | 
 | 36 |  | 
 | 37 |  | 
 | 38 | # Find js scaper script | 
 | 39 | SCRAPE_JS = os.path.join(os.path.dirname(__file__), '..', 'lib', 'build_perf', | 
 | 40 |                          'scrape-html-report.js') | 
 | 41 | if not os.path.isfile(SCRAPE_JS): | 
 | 42 |     log.error("Unableto find oe-build-perf-report-scrape.js") | 
 | 43 |     sys.exit(1) | 
 | 44 |  | 
 | 45 |  | 
 | 46 | class ReportError(Exception): | 
 | 47 |     """Local errors""" | 
 | 48 |     pass | 
 | 49 |  | 
 | 50 |  | 
 | 51 | def check_utils(): | 
 | 52 |     """Check that all needed utils are installed in the system""" | 
 | 53 |     missing = [] | 
 | 54 |     for cmd in ('phantomjs', 'optipng'): | 
 | 55 |         if not shutil.which(cmd): | 
 | 56 |             missing.append(cmd) | 
 | 57 |     if missing: | 
 | 58 |         log.error("The following tools are missing: %s", ' '.join(missing)) | 
 | 59 |         sys.exit(1) | 
 | 60 |  | 
 | 61 |  | 
 | 62 | def parse_args(argv): | 
 | 63 |     """Parse command line arguments""" | 
 | 64 |     description = """Email build perf test report""" | 
 | 65 |     parser = argparse.ArgumentParser( | 
 | 66 |         formatter_class=argparse.ArgumentDefaultsHelpFormatter, | 
 | 67 |         description=description) | 
 | 68 |  | 
 | 69 |     parser.add_argument('--debug', '-d', action='store_true', | 
 | 70 |                         help="Verbose logging") | 
 | 71 |     parser.add_argument('--quiet', '-q', action='store_true', | 
 | 72 |                         help="Only print errors") | 
 | 73 |     parser.add_argument('--to', action='append', | 
 | 74 |                         help="Recipients of the email") | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 75 |     parser.add_argument('--cc', action='append', | 
 | 76 |                         help="Carbon copy recipients of the email") | 
 | 77 |     parser.add_argument('--bcc', action='append', | 
 | 78 |                         help="Blind carbon copy recipients of the email") | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 79 |     parser.add_argument('--subject', default="Yocto build perf test report", | 
 | 80 |                         help="Email subject") | 
 | 81 |     parser.add_argument('--outdir', '-o', | 
 | 82 |                         help="Store files in OUTDIR. Can be used to preserve " | 
 | 83 |                              "the email parts") | 
 | 84 |     parser.add_argument('--text', | 
 | 85 |                         help="Plain text message") | 
 | 86 |     parser.add_argument('--html', | 
 | 87 |                         help="HTML peport generated by oe-build-perf-report") | 
 | 88 |     parser.add_argument('--phantomjs-args', action='append', | 
 | 89 |                         help="Extra command line arguments passed to PhantomJS") | 
 | 90 |  | 
 | 91 |     args = parser.parse_args(argv) | 
 | 92 |  | 
 | 93 |     if not args.html and not args.text: | 
 | 94 |         parser.error("Please specify --html and/or --text") | 
 | 95 |  | 
 | 96 |     return args | 
 | 97 |  | 
 | 98 |  | 
 | 99 | def decode_png(infile, outfile): | 
 | 100 |     """Parse/decode/optimize png data from a html element""" | 
 | 101 |     with open(infile) as f: | 
 | 102 |         raw_data = f.read() | 
 | 103 |  | 
 | 104 |     # Grab raw base64 data | 
 | 105 |     b64_data = re.sub('^.*href="data:image/png;base64,', '', raw_data, 1) | 
 | 106 |     b64_data = re.sub('">.+$', '', b64_data, 1) | 
 | 107 |  | 
 | 108 |     # Replace file with proper decoded png | 
 | 109 |     with open(outfile, 'wb') as f: | 
 | 110 |         f.write(base64.b64decode(b64_data)) | 
 | 111 |  | 
 | 112 |     subprocess.check_output(['optipng', outfile], stderr=subprocess.STDOUT) | 
 | 113 |  | 
 | 114 |  | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 115 | def mangle_html_report(infile, outfile, pngs): | 
 | 116 |     """Mangle html file into a email compatible format""" | 
 | 117 |     paste = True | 
 | 118 |     png_dir = os.path.dirname(outfile) | 
 | 119 |     with open(infile) as f_in: | 
 | 120 |         with open(outfile, 'w') as f_out: | 
 | 121 |             for line in f_in.readlines(): | 
 | 122 |                 stripped = line.strip() | 
 | 123 |                 # Strip out scripts | 
 | 124 |                 if stripped == '<!--START-OF-SCRIPTS-->': | 
 | 125 |                     paste = False | 
 | 126 |                 elif stripped == '<!--END-OF-SCRIPTS-->': | 
 | 127 |                     paste = True | 
 | 128 |                 elif paste: | 
 | 129 |                     if re.match('^.+href="data:image/png;base64', stripped): | 
 | 130 |                         # Strip out encoded pngs (as they're huge in size) | 
 | 131 |                         continue | 
 | 132 |                     elif 'www.gstatic.com' in stripped: | 
 | 133 |                         # HACK: drop references to external static pages | 
 | 134 |                         continue | 
 | 135 |  | 
 | 136 |                     # Replace charts with <img> elements | 
 | 137 |                     match = re.match('<div id="(?P<id>\w+)"', stripped) | 
 | 138 |                     if match and match.group('id') in pngs: | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 139 |                         f_out.write('<img src="cid:{}"\n'.format(match.group('id'))) | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 140 |                     else: | 
 | 141 |                         f_out.write(line) | 
 | 142 |  | 
 | 143 |  | 
 | 144 | def scrape_html_report(report, outdir, phantomjs_extra_args=None): | 
 | 145 |     """Scrape html report into a format sendable by email""" | 
 | 146 |     tmpdir = tempfile.mkdtemp(dir='.') | 
 | 147 |     log.debug("Using tmpdir %s for phantomjs output", tmpdir) | 
 | 148 |  | 
 | 149 |     if not os.path.isdir(outdir): | 
 | 150 |         os.mkdir(outdir) | 
 | 151 |     if os.path.splitext(report)[1] not in ('.html', '.htm'): | 
 | 152 |         raise ReportError("Invalid file extension for report, needs to be " | 
 | 153 |                           "'.html' or '.htm'") | 
 | 154 |  | 
 | 155 |     try: | 
 | 156 |         log.info("Scraping HTML report with PhangomJS") | 
 | 157 |         extra_args = phantomjs_extra_args if phantomjs_extra_args else [] | 
 | 158 |         subprocess.check_output(['phantomjs', '--debug=true'] + extra_args + | 
 | 159 |                                 [SCRAPE_JS, report, tmpdir], | 
 | 160 |                                 stderr=subprocess.STDOUT) | 
 | 161 |  | 
 | 162 |         pngs = [] | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 163 |         images = [] | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 164 |         for fname in os.listdir(tmpdir): | 
 | 165 |             base, ext = os.path.splitext(fname) | 
 | 166 |             if ext == '.png': | 
 | 167 |                 log.debug("Decoding %s", fname) | 
 | 168 |                 decode_png(os.path.join(tmpdir, fname), | 
 | 169 |                            os.path.join(outdir, fname)) | 
 | 170 |                 pngs.append(base) | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 171 |                 images.append(fname) | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 172 |             elif ext in ('.html', '.htm'): | 
 | 173 |                 report_file = fname | 
 | 174 |             else: | 
 | 175 |                 log.warning("Unknown file extension: '%s'", ext) | 
 | 176 |                 #shutil.move(os.path.join(tmpdir, fname), outdir) | 
 | 177 |  | 
 | 178 |         log.debug("Mangling html report file %s", report_file) | 
 | 179 |         mangle_html_report(os.path.join(tmpdir, report_file), | 
 | 180 |                            os.path.join(outdir, report_file), pngs) | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 181 |         return (os.path.join(outdir, report_file), | 
 | 182 |                 [os.path.join(outdir, i) for i in images]) | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 183 |     finally: | 
 | 184 |         shutil.rmtree(tmpdir) | 
 | 185 |  | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 186 | def send_email(text_fn, html_fn, image_fns, subject, recipients, copy=[], | 
 | 187 |                blind_copy=[]): | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 188 |     """Send email""" | 
 | 189 |     # Generate email message | 
 | 190 |     text_msg = html_msg = None | 
 | 191 |     if text_fn: | 
 | 192 |         with open(text_fn) as f: | 
 | 193 |             text_msg = MIMEText("Yocto build performance test report.\n" + | 
 | 194 |                                 f.read(), 'plain') | 
 | 195 |     if html_fn: | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 196 |         html_msg = msg = MIMEMultipart('related') | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 197 |         with open(html_fn) as f: | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 198 |             html_msg.attach(MIMEText(f.read(), 'html')) | 
 | 199 |         for img_fn in image_fns: | 
 | 200 |             # Expect that content id is same as the filename | 
 | 201 |             cid = os.path.splitext(os.path.basename(img_fn))[0] | 
 | 202 |             with open(img_fn, 'rb') as f: | 
 | 203 |                 image_msg = MIMEImage(f.read()) | 
 | 204 |             image_msg['Content-ID'] = '<{}>'.format(cid) | 
 | 205 |             html_msg.attach(image_msg) | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 206 |  | 
 | 207 |     if text_msg and html_msg: | 
 | 208 |         msg = MIMEMultipart('alternative') | 
 | 209 |         msg.attach(text_msg) | 
 | 210 |         msg.attach(html_msg) | 
 | 211 |     elif text_msg: | 
 | 212 |         msg = text_msg | 
 | 213 |     elif html_msg: | 
 | 214 |         msg = html_msg | 
 | 215 |     else: | 
 | 216 |         raise ReportError("Neither plain text nor html body specified") | 
 | 217 |  | 
 | 218 |     pw_data = pwd.getpwuid(os.getuid()) | 
 | 219 |     full_name = pw_data.pw_gecos.split(',')[0] | 
 | 220 |     email = os.environ.get('EMAIL', | 
 | 221 |                            '{}@{}'.format(pw_data.pw_name, socket.getfqdn())) | 
 | 222 |     msg['From'] = "{} <{}>".format(full_name, email) | 
 | 223 |     msg['To'] = ', '.join(recipients) | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 224 |     if copy: | 
 | 225 |         msg['Cc'] = ', '.join(copy) | 
 | 226 |     if blind_copy: | 
 | 227 |         msg['Bcc'] = ', '.join(blind_copy) | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 228 |     msg['Subject'] = subject | 
 | 229 |  | 
 | 230 |     # Send email | 
 | 231 |     with smtplib.SMTP('localhost') as smtp: | 
 | 232 |         smtp.send_message(msg) | 
 | 233 |  | 
 | 234 |  | 
 | 235 | def main(argv=None): | 
 | 236 |     """Script entry point""" | 
 | 237 |     args = parse_args(argv) | 
 | 238 |     if args.quiet: | 
 | 239 |         log.setLevel(logging.ERROR) | 
 | 240 |     if args.debug: | 
 | 241 |         log.setLevel(logging.DEBUG) | 
 | 242 |  | 
 | 243 |     check_utils() | 
 | 244 |  | 
 | 245 |     if args.outdir: | 
 | 246 |         outdir = args.outdir | 
 | 247 |         if not os.path.exists(outdir): | 
 | 248 |             os.mkdir(outdir) | 
 | 249 |     else: | 
 | 250 |         outdir = tempfile.mkdtemp(dir='.') | 
 | 251 |  | 
 | 252 |     try: | 
 | 253 |         log.debug("Storing email parts in %s", outdir) | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 254 |         html_report = images = None | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 255 |         if args.html: | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 256 |             html_report, images = scrape_html_report(args.html, outdir, | 
 | 257 |                                                      args.phantomjs_args) | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 258 |  | 
 | 259 |         if args.to: | 
 | 260 |             log.info("Sending email to %s", ', '.join(args.to)) | 
| Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 261 |             if args.cc: | 
 | 262 |                 log.info("Copying to %s", ', '.join(args.cc)) | 
 | 263 |             if args.bcc: | 
 | 264 |                 log.info("Blind copying to %s", ', '.join(args.bcc)) | 
 | 265 |             send_email(args.text, html_report, images, args.subject, | 
 | 266 |                        args.to, args.cc, args.bcc) | 
| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 267 |     except subprocess.CalledProcessError as err: | 
 | 268 |         log.error("%s, with output:\n%s", str(err), err.output.decode()) | 
 | 269 |         return 1 | 
 | 270 |     except ReportError as err: | 
 | 271 |         log.error(err) | 
 | 272 |         return 1 | 
 | 273 |     finally: | 
 | 274 |         if not args.outdir: | 
 | 275 |             log.debug("Wiping %s", outdir) | 
 | 276 |             shutil.rmtree(outdir) | 
 | 277 |  | 
 | 278 |     return 0 | 
 | 279 |  | 
 | 280 |  | 
 | 281 | if __name__ == "__main__": | 
 | 282 |     sys.exit(main()) |