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