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