Merge pull request #577 from mwhudson/error-report-send-to-daisy

enable sending reports to daisy
This commit is contained in:
Michael Hudson-Doyle 2019-12-06 09:26:21 +13:00 committed by GitHub
commit 4ce4e8b48f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 210 additions and 21 deletions

View File

@ -4,6 +4,6 @@ set -eux
cat /etc/apt/sources.list | sed -n 's/-updates/-proposed/p' > /etc/apt/sources.list.d/proposed.list
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -y dist-upgrade
apt-get install -y --no-install-recommends libnl-3-dev libnl-genl-3-dev libnl-route-3-dev libsystemd-dev python3-distutils-extra pkg-config python3.5 python3-pip git lsb-release python3-setuptools gcc python3-dev python3-wheel curtin pep8 python3-pyflakes
apt-get install -y --no-install-recommends libnl-3-dev libnl-genl-3-dev libnl-route-3-dev libsystemd-dev python3-distutils-extra pkg-config python3.5 python3-pip git lsb-release python3-setuptools gcc python3-dev python3-wheel curtin pep8 python3-pyflakes python3-bson
pip3 install -r requirements.txt
python3 setup.py build

View File

@ -9,6 +9,7 @@ from systemd import journal
json_file = sys.argv[1]
event_identifier = sys.argv[2]
log_location = sys.argv[3]
scale_factor = float(os.environ.get('SUBIQUITY_REPLAY_TIMESCALE', "4"))
@ -17,7 +18,7 @@ def time_for_entry(e):
rc = 0
def report(e):
def report(e, log_file):
global rc
if e['SYSLOG_IDENTIFIER'].startswith("curtin_event"):
e['SYSLOG_IDENTIFIER'] = event_identifier
@ -30,13 +31,17 @@ def report(e):
rc = 1
elif e['SYSLOG_IDENTIFIER'].startswith("curtin_log") and scale_factor < 10:
print(e['MESSAGE'], flush=True)
log_file.write(e['MESSAGE'] + '\n')
with open(log_location, 'w') as fp:
prev_ev = None
for line in open(json_file):
ev = json.loads(line.strip())
if prev_ev is not None:
report(prev_ev, fp)
delay = time_for_entry(ev) - time_for_entry(prev_ev)
time.sleep(min(delay, 8)/scale_factor)
prev_ev = ev
report(ev, fp)
prev_ev = None
for line in open(json_file):
ev = json.loads(line.strip())
if prev_ev is not None:
report(prev_ev)
time.sleep(min((time_for_entry(ev) - time_for_entry(prev_ev)), 8)/scale_factor)
prev_ev = ev
report(ev)
sys.exit(rc)

View File

@ -50,6 +50,7 @@ parts:
- iso-codes
- lsb-release
- python3-apport
- python3-bson
- python3-distutils-extra
- python3-urwid
- python3-requests

View File

@ -23,8 +23,12 @@ import apport
import apport.crashdb
import apport.hookutils
import bson
import attr
import requests
import urwid
from subiquitycore.controller import BaseController
@ -49,6 +53,33 @@ class ErrorReportKind(enum.Enum):
UNKNOWN = _("Unknown error")
@attr.s(cmp=False)
class Upload(metaclass=urwid.MetaSignals):
signals = ['progress']
controller = attr.ib()
bytes_to_send = attr.ib()
bytes_sent = attr.ib(default=0)
pipe_w = attr.ib(default=None)
cancelled = attr.ib(default=False)
def start(self):
self.pipe_w = self.controller.loop.watch_pipe(self._progress)
def _progress(self, x):
urwid.emit_signal(self, 'progress')
def _bg_update(self, sent, to_send=None):
self.bytes_sent = sent
if to_send is not None:
self.bytes_to_send = to_send
os.write(self.pipe_w, b'x')
def stop(self):
self.controller.loop.remove_watch_pipe(self.pipe_w)
os.close(self.pipe_w)
@attr.s(cmp=False)
class ErrorReport(metaclass=urwid.MetaSignals):
@ -61,6 +92,7 @@ class ErrorReport(metaclass=urwid.MetaSignals):
_file = attr.ib()
meta = attr.ib(default=attr.Factory(dict))
uploader = attr.ib(default=None)
@classmethod
def new(cls, controller, kind):
@ -160,6 +192,67 @@ class ErrorReport(metaclass=urwid.MetaSignals):
cb()
self.controller.run_in_bg(_bg_load, loaded)
def upload(self):
log.debug("starting upload for %s", self.base)
uploader = self.uploader = Upload(
controller=self.controller, bytes_to_send=1)
url = "https://daisy.ubuntu.com"
if self.controller.opts.dry_run:
url = "https://daisy.staging.ubuntu.com"
chunk_size = 1024
def chunk(data):
for i in range(0, len(data), chunk_size):
if uploader.cancelled:
log.debug("upload for %s cancelled", self.base)
return
yield data[i:i+chunk_size]
uploader._bg_update(uploader.bytes_sent + chunk_size)
def _bg_upload():
for_upload = {
"Kind": self.kind.value
}
for k, v in self.pr.items():
if len(v) < 1024 or k in {"Traceback", "ProcCpuinfoMinimal"}:
for_upload[k] = v
else:
log.debug("dropping %s of length %s", k, len(v))
if "CurtinLog" in self.pr:
logtail = []
for line in self.pr["CurtinLog"].splitlines():
logtail.append(line.strip())
while sum(map(len, logtail)) > 2048:
logtail.pop(0)
for_upload["CurtinLogTail"] = "\n".join(logtail)
data = bson.BSON().encode(for_upload)
self.uploader._bg_update(0, len(data))
headers = {
'user-agent': 'subiquity/{}'.format(
os.environ.get("SNAP_VERSION", "SNAP_VERSION")),
}
response = requests.post(url, data=chunk(data), headers=headers)
response.raise_for_status()
return response.text.split()[0]
def uploaded(fut):
try:
oops_id = fut.result()
except requests.exceptions.RequestException:
log.exception("upload for %s failed", self.base)
else:
log.debug("finished upload for %s, %r", self.base, oops_id)
self.set_meta("oops-id", oops_id)
uploader.stop()
self.uploader = None
urwid.emit_signal(self, 'changed')
urwid.emit_signal(self, 'changed')
uploader.start()
self.controller.run_in_bg(_bg_upload, uploaded)
def _path_with_ext(self, ext):
return os.path.join(
self.controller.crash_directory, self.base + '.' + ext)
@ -190,6 +283,10 @@ class ErrorReport(metaclass=urwid.MetaSignals):
def seen(self):
return self.meta.get("seen", False)
@property
def oops_id(self):
return self.meta.get("oops-id")
@property
def persistent_details(self):
"""Return fs-label, path-on-fs to report."""

View File

@ -18,6 +18,7 @@ from concurrent.futures import Future
import datetime
import logging
import os
import re
import signal
import subprocess
import sys
@ -205,6 +206,25 @@ class StateMachine:
self.run()
class TracebackExtractor:
start_marker = re.compile(r"^Traceback \(most recent call last\):")
end_marker = re.compile(r"\S")
def __init__(self):
self.traceback = []
self.in_traceback = False
def feed(self, line):
if not self.traceback and self.start_marker.match(line):
self.in_traceback = True
elif self.in_traceback and self.end_marker.match(line):
self.traceback.append(line)
self.in_traceback = False
if self.in_traceback:
self.traceback.append(line)
class InstallProgressController(BaseController):
signals = [
('installprogress:filesystem-config-done', 'filesystem_config_done'),
@ -230,6 +250,7 @@ class InstallProgressController(BaseController):
self._event_syslog_identifier = 'curtin_event.%s' % (os.getpid(),)
self._log_syslog_identifier = 'curtin_log.%s' % (os.getpid(),)
self.sm = None
self.tb_extractor = TracebackExtractor()
def tpath(self, *path):
return os.path.join(self.model.target, *path)
@ -254,8 +275,12 @@ class InstallProgressController(BaseController):
def curtin_error(self):
self.install_state = InstallState.ERROR
kw = {}
if self.tb_extractor.traceback:
kw["Traceback"] = "\n".join(self.tb_extractor.traceback)
crash_report = self.app.make_apport_report(
ErrorReportKind.INSTALL_FAIL, "install failed", interrupt=False)
ErrorReportKind.INSTALL_FAIL, "install failed", interrupt=False,
**kw)
self.progress_view.spinner.stop()
if sys.exc_info()[0] is not None:
self.progress_view.add_log_line(traceback.format_exc())
@ -301,7 +326,9 @@ class InstallProgressController(BaseController):
self._install_event_finish()
def curtin_log(self, event):
self.progress_view.add_log_line(event['MESSAGE'])
log_line = event['MESSAGE']
self.progress_view.add_log_line(log_line)
self.tb_extractor.feed(log_line)
def start_journald_listener(self, identifiers, callback):
reader = journal.Reader()
@ -329,23 +356,27 @@ class InstallProgressController(BaseController):
if self.opts.dry_run:
config_location = os.path.join('.subiquity/', config_file_name)
log_location = '.subiquity/install.log'
event_file = "examples/curtin-events.json"
if 'install-fail' in self.debug_flags:
event_file = "examples/curtin-events-fail.json"
curtin_cmd = ["python3", "scripts/replay-curtin-log.py",
event_file, self._event_syslog_identifier]
curtin_cmd = [
"python3", "scripts/replay-curtin-log.py", event_file,
self._event_syslog_identifier, log_location,
]
else:
config_location = os.path.join('/var/log/installer',
config_file_name)
curtin_cmd = [sys.executable, '-m', 'curtin', '--showtrace', '-c',
config_location, 'install']
log_location = INSTALL_LOG
ident = self._event_syslog_identifier
self._write_config(config_location,
self.model.render(syslog_identifier=ident))
self.app.note_file_for_apport("CurtinConfig", config_location)
self.app.note_file_for_apport("CurtinLog", INSTALL_LOG)
self.app.note_file_for_apport("CurtinLog", log_location)
self.app.note_file_for_apport("CurtinErrors", ERROR_TARFILE)
return curtin_cmd

View File

@ -176,7 +176,7 @@ class Subiquity(Application):
def note_data_for_apport(self, key, value):
self._apport_data.append((key, value))
def make_apport_report(self, kind, thing, *, interrupt, wait=False):
def make_apport_report(self, kind, thing, *, interrupt, wait=False, **kw):
log.debug("generating crash report")
try:
@ -203,6 +203,8 @@ class Subiquity(Application):
apport.hookutils.attach_file_if_exists(report.pr, path, key)
for key, value in apport_data:
report.pr[key] = value
for key, value in kw.items():
report.pr[key] = value
report.add_info(_bg_attach_hook, wait)

View File

@ -19,6 +19,7 @@ from urwid import (
connect_signal,
disconnect_signal,
Padding,
ProgressBar,
Text,
)
@ -37,6 +38,7 @@ from subiquitycore.ui.utils import (
button_pile,
ClickableIcon,
Color,
disabled,
rewrap,
)
from subiquitycore.ui.width import (
@ -112,6 +114,11 @@ Do you want to try starting the installation again?
}
submit_text = _("""
If you want to help improve the installer, you can send an error report.
""")
class ErrorReportStretchy(Stretchy):
def __init__(self, app, parent, report, interrupting=True):
@ -121,12 +128,17 @@ class ErrorReportStretchy(Stretchy):
self.interrupting = interrupting
self.btns = {
'cancel': other_btn(
_("Cancel upload"), on_press=self.cancel_upload),
'close': close_btn(parent, _("Close report")),
'continue': close_btn(parent, _("Continue")),
'debug_shell': other_btn(
_("Switch to a shell"), on_press=self.debug_shell),
'restart': other_btn(
_("Restart the installer"), on_press=self.restart),
'submit': other_btn(
_("Send to Canonical"), on_press=self.submit),
'submitted': disabled(other_btn(_("Sent to Canonical"))),
'view': other_btn(
_("View full report"), on_press=self.view_report),
}
@ -142,7 +154,26 @@ class ErrorReportStretchy(Stretchy):
super().__init__("", [self.pile], 0, 0)
connect_signal(self, 'closed', self.spinner.stop)
def pb(self, upload):
pb = ProgressBar(
normal='progress_incomplete',
complete='progress_complete',
current=upload.bytes_sent,
done=upload.bytes_to_send)
def _progress():
pb.done = upload.bytes_to_send
pb.current = upload.bytes_sent
connect_signal(upload, 'progress', _progress)
return pb
def _pile_elements(self):
btns = self.btns.copy()
if self.report.uploader:
btns['continue'] = btns['close'] = btns['cancel']
widgets = [
Text(rewrap(_(error_report_intros[self.report.kind]))),
Text(""),
@ -151,7 +182,21 @@ class ErrorReportStretchy(Stretchy):
self.spinner.stop()
if self.report.state == ErrorReportState.DONE:
widgets.append(self.btns['view'])
widgets.append(btns['view'])
widgets.append(Text(""))
widgets.append(Text(rewrap(_(submit_text))))
widgets.append(Text(""))
if self.report.uploader:
if self.upload_pb is None:
self.upload_pb = self.pb(self.report.uploader)
widgets.append(self.upload_pb)
else:
if self.report.oops_id:
widgets.append(btns['submitted'])
else:
widgets.append(btns['submit'])
self.upload_pb = None
fs_label, fs_loc = self.report.persistent_details
if fs_label is not None:
@ -174,15 +219,15 @@ class ErrorReportStretchy(Stretchy):
if self.interrupting:
if self.report.state != ErrorReportState.INCOMPLETE:
text, btns = error_report_options[self.report.kind]
text, btn_names = error_report_options[self.report.kind]
if text:
widgets.extend([Text(""), Text(rewrap(_(text)))])
for b in btns:
widgets.extend([Text(""), self.btns[b]])
for b in btn_names:
widgets.extend([Text(""), btns[b]])
else:
widgets.extend([
Text(""),
self.btns['close'],
btns['close'],
])
return widgets
@ -206,6 +251,14 @@ class ErrorReportStretchy(Stretchy):
def view_report(self, sender):
self.app.run_command_in_foreground(["less", self.report.path])
def submit(self, sender):
self.report.upload()
def cancel_upload(self, sender):
self.report.uploader.cancelled = True
self.report.uploader = None
self._report_changed()
def opened(self):
self.report.mark_seen()
connect_signal(self.report, 'changed', self._report_changed)