Merge pull request #577 from mwhudson/error-report-send-to-daisy
enable sending reports to daisy
This commit is contained in:
commit
4ce4e8b48f
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -50,6 +50,7 @@ parts:
|
|||
- iso-codes
|
||||
- lsb-release
|
||||
- python3-apport
|
||||
- python3-bson
|
||||
- python3-distutils-extra
|
||||
- python3-urwid
|
||||
- python3-requests
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue