| #!/usr/bin/python3 |
| # |
| # Send build performance test report emails |
| # |
| # Copyright (c) 2017, Intel Corporation. |
| # |
| # SPDX-License-Identifier: GPL-2.0-only |
| # |
| |
| import argparse |
| import base64 |
| import logging |
| import os |
| import pwd |
| import re |
| import shutil |
| import smtplib |
| import socket |
| import subprocess |
| import sys |
| import tempfile |
| from email.mime.image import MIMEImage |
| from email.mime.multipart import MIMEMultipart |
| from email.mime.text import MIMEText |
| |
| |
| # Setup logging |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") |
| log = logging.getLogger('oe-build-perf-report') |
| |
| |
| # Find js scaper script |
| SCRAPE_JS = os.path.join(os.path.dirname(__file__), '..', 'lib', 'build_perf', |
| 'scrape-html-report.js') |
| if not os.path.isfile(SCRAPE_JS): |
| log.error("Unableto find oe-build-perf-report-scrape.js") |
| sys.exit(1) |
| |
| |
| class ReportError(Exception): |
| """Local errors""" |
| pass |
| |
| |
| def check_utils(): |
| """Check that all needed utils are installed in the system""" |
| missing = [] |
| for cmd in ('phantomjs', 'optipng'): |
| if not shutil.which(cmd): |
| missing.append(cmd) |
| if missing: |
| log.error("The following tools are missing: %s", ' '.join(missing)) |
| sys.exit(1) |
| |
| |
| def parse_args(argv): |
| """Parse command line arguments""" |
| description = """Email build perf test report""" |
| parser = argparse.ArgumentParser( |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, |
| description=description) |
| |
| parser.add_argument('--debug', '-d', action='store_true', |
| help="Verbose logging") |
| parser.add_argument('--quiet', '-q', action='store_true', |
| help="Only print errors") |
| parser.add_argument('--to', action='append', |
| help="Recipients of the email") |
| parser.add_argument('--cc', action='append', |
| help="Carbon copy recipients of the email") |
| parser.add_argument('--bcc', action='append', |
| help="Blind carbon copy recipients of the email") |
| parser.add_argument('--subject', default="Yocto build perf test report", |
| help="Email subject") |
| parser.add_argument('--outdir', '-o', |
| help="Store files in OUTDIR. Can be used to preserve " |
| "the email parts") |
| parser.add_argument('--text', |
| help="Plain text message") |
| parser.add_argument('--html', |
| help="HTML peport generated by oe-build-perf-report") |
| parser.add_argument('--phantomjs-args', action='append', |
| help="Extra command line arguments passed to PhantomJS") |
| |
| args = parser.parse_args(argv) |
| |
| if not args.html and not args.text: |
| parser.error("Please specify --html and/or --text") |
| |
| return args |
| |
| |
| def decode_png(infile, outfile): |
| """Parse/decode/optimize png data from a html element""" |
| with open(infile) as f: |
| raw_data = f.read() |
| |
| # Grab raw base64 data |
| b64_data = re.sub('^.*href="data:image/png;base64,', '', raw_data, 1) |
| b64_data = re.sub('">.+$', '', b64_data, 1) |
| |
| # Replace file with proper decoded png |
| with open(outfile, 'wb') as f: |
| f.write(base64.b64decode(b64_data)) |
| |
| subprocess.check_output(['optipng', outfile], stderr=subprocess.STDOUT) |
| |
| |
| def mangle_html_report(infile, outfile, pngs): |
| """Mangle html file into a email compatible format""" |
| paste = True |
| png_dir = os.path.dirname(outfile) |
| with open(infile) as f_in: |
| with open(outfile, 'w') as f_out: |
| for line in f_in.readlines(): |
| stripped = line.strip() |
| # Strip out scripts |
| if stripped == '<!--START-OF-SCRIPTS-->': |
| paste = False |
| elif stripped == '<!--END-OF-SCRIPTS-->': |
| paste = True |
| elif paste: |
| if re.match('^.+href="data:image/png;base64', stripped): |
| # Strip out encoded pngs (as they're huge in size) |
| continue |
| elif 'www.gstatic.com' in stripped: |
| # HACK: drop references to external static pages |
| continue |
| |
| # Replace charts with <img> elements |
| match = re.match('<div id="(?P<id>\w+)"', stripped) |
| if match and match.group('id') in pngs: |
| f_out.write('<img src="cid:{}"\n'.format(match.group('id'))) |
| else: |
| f_out.write(line) |
| |
| |
| def scrape_html_report(report, outdir, phantomjs_extra_args=None): |
| """Scrape html report into a format sendable by email""" |
| tmpdir = tempfile.mkdtemp(dir='.') |
| log.debug("Using tmpdir %s for phantomjs output", tmpdir) |
| |
| if not os.path.isdir(outdir): |
| os.mkdir(outdir) |
| if os.path.splitext(report)[1] not in ('.html', '.htm'): |
| raise ReportError("Invalid file extension for report, needs to be " |
| "'.html' or '.htm'") |
| |
| try: |
| log.info("Scraping HTML report with PhangomJS") |
| extra_args = phantomjs_extra_args if phantomjs_extra_args else [] |
| subprocess.check_output(['phantomjs', '--debug=true'] + extra_args + |
| [SCRAPE_JS, report, tmpdir], |
| stderr=subprocess.STDOUT) |
| |
| pngs = [] |
| images = [] |
| for fname in os.listdir(tmpdir): |
| base, ext = os.path.splitext(fname) |
| if ext == '.png': |
| log.debug("Decoding %s", fname) |
| decode_png(os.path.join(tmpdir, fname), |
| os.path.join(outdir, fname)) |
| pngs.append(base) |
| images.append(fname) |
| elif ext in ('.html', '.htm'): |
| report_file = fname |
| else: |
| log.warning("Unknown file extension: '%s'", ext) |
| #shutil.move(os.path.join(tmpdir, fname), outdir) |
| |
| log.debug("Mangling html report file %s", report_file) |
| mangle_html_report(os.path.join(tmpdir, report_file), |
| os.path.join(outdir, report_file), pngs) |
| return (os.path.join(outdir, report_file), |
| [os.path.join(outdir, i) for i in images]) |
| finally: |
| shutil.rmtree(tmpdir) |
| |
| def send_email(text_fn, html_fn, image_fns, subject, recipients, copy=[], |
| blind_copy=[]): |
| """Send email""" |
| # Generate email message |
| text_msg = html_msg = None |
| if text_fn: |
| with open(text_fn) as f: |
| text_msg = MIMEText("Yocto build performance test report.\n" + |
| f.read(), 'plain') |
| if html_fn: |
| html_msg = msg = MIMEMultipart('related') |
| with open(html_fn) as f: |
| html_msg.attach(MIMEText(f.read(), 'html')) |
| for img_fn in image_fns: |
| # Expect that content id is same as the filename |
| cid = os.path.splitext(os.path.basename(img_fn))[0] |
| with open(img_fn, 'rb') as f: |
| image_msg = MIMEImage(f.read()) |
| image_msg['Content-ID'] = '<{}>'.format(cid) |
| html_msg.attach(image_msg) |
| |
| if text_msg and html_msg: |
| msg = MIMEMultipart('alternative') |
| msg.attach(text_msg) |
| msg.attach(html_msg) |
| elif text_msg: |
| msg = text_msg |
| elif html_msg: |
| msg = html_msg |
| else: |
| raise ReportError("Neither plain text nor html body specified") |
| |
| pw_data = pwd.getpwuid(os.getuid()) |
| full_name = pw_data.pw_gecos.split(',')[0] |
| email = os.environ.get('EMAIL', |
| '{}@{}'.format(pw_data.pw_name, socket.getfqdn())) |
| msg['From'] = "{} <{}>".format(full_name, email) |
| msg['To'] = ', '.join(recipients) |
| if copy: |
| msg['Cc'] = ', '.join(copy) |
| if blind_copy: |
| msg['Bcc'] = ', '.join(blind_copy) |
| msg['Subject'] = subject |
| |
| # Send email |
| with smtplib.SMTP('localhost') as smtp: |
| smtp.send_message(msg) |
| |
| |
| def main(argv=None): |
| """Script entry point""" |
| args = parse_args(argv) |
| if args.quiet: |
| log.setLevel(logging.ERROR) |
| if args.debug: |
| log.setLevel(logging.DEBUG) |
| |
| check_utils() |
| |
| if args.outdir: |
| outdir = args.outdir |
| if not os.path.exists(outdir): |
| os.mkdir(outdir) |
| else: |
| outdir = tempfile.mkdtemp(dir='.') |
| |
| try: |
| log.debug("Storing email parts in %s", outdir) |
| html_report = images = None |
| if args.html: |
| html_report, images = scrape_html_report(args.html, outdir, |
| args.phantomjs_args) |
| |
| if args.to: |
| log.info("Sending email to %s", ', '.join(args.to)) |
| if args.cc: |
| log.info("Copying to %s", ', '.join(args.cc)) |
| if args.bcc: |
| log.info("Blind copying to %s", ', '.join(args.bcc)) |
| send_email(args.text, html_report, images, args.subject, |
| args.to, args.cc, args.bcc) |
| except subprocess.CalledProcessError as err: |
| log.error("%s, with output:\n%s", str(err), err.output.decode()) |
| return 1 |
| except ReportError as err: |
| log.error(err) |
| return 1 |
| finally: |
| if not args.outdir: |
| log.debug("Wiping %s", outdir) |
| shutil.rmtree(outdir) |
| |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |