add subiqity.common package and more error report handling to it
Both the subiquity client and server are going to want to use this code, so move it somewhere more neutral.
This commit is contained in:
parent
16e080392e
commit
2e5456fd55
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2020 Canonical, Ltd.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
@ -0,0 +1,412 @@
|
|||
# Copyright 2020 Canonical, Ltd.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import asyncio
|
||||
import enum
|
||||
import fcntl
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import apport
|
||||
import apport.crashdb
|
||||
import apport.hookutils
|
||||
|
||||
import attr
|
||||
|
||||
import bson
|
||||
|
||||
import requests
|
||||
|
||||
import urwid
|
||||
|
||||
from subiquitycore.async_helpers import (
|
||||
run_in_thread,
|
||||
schedule_task,
|
||||
)
|
||||
|
||||
log = logging.getLogger('subiquitycore.common.errorreport')
|
||||
|
||||
|
||||
class ErrorReportState(enum.Enum):
|
||||
INCOMPLETE = enum.auto()
|
||||
LOADING = enum.auto()
|
||||
DONE = enum.auto()
|
||||
ERROR_GENERATING = enum.auto()
|
||||
ERROR_LOADING = enum.auto()
|
||||
|
||||
|
||||
class ErrorReportKind(enum.Enum):
|
||||
BLOCK_PROBE_FAIL = _("Block device probe failure")
|
||||
DISK_PROBE_FAIL = _("Disk probe failure")
|
||||
INSTALL_FAIL = _("Install failure")
|
||||
UI = _("Installer crash")
|
||||
NETWORK_FAIL = _("Network error")
|
||||
UNKNOWN = _("Unknown error")
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class Upload(metaclass=urwid.MetaSignals):
|
||||
signals = ['progress']
|
||||
|
||||
bytes_to_send = attr.ib()
|
||||
bytes_sent = attr.ib(default=0)
|
||||
pipe_r = attr.ib(default=None)
|
||||
pipe_w = attr.ib(default=None)
|
||||
cancelled = attr.ib(default=False)
|
||||
|
||||
def start(self):
|
||||
self.pipe_r, self.pipe_w = os.pipe()
|
||||
fcntl.fcntl(self.pipe_r, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
asyncio.get_event_loop().add_reader(self.pipe_r, self._progress)
|
||||
|
||||
def _progress(self):
|
||||
os.read(self.pipe_w, 4096)
|
||||
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):
|
||||
asyncio.get_event_loop().remove_reader(self.pipe_r)
|
||||
os.close(self.pipe_w)
|
||||
os.close(self.pipe_r)
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class ErrorReport(metaclass=urwid.MetaSignals):
|
||||
|
||||
signals = ["changed"]
|
||||
|
||||
reporter = attr.ib()
|
||||
base = attr.ib()
|
||||
pr = attr.ib()
|
||||
state = attr.ib()
|
||||
_file = attr.ib()
|
||||
_context = attr.ib()
|
||||
|
||||
meta = attr.ib(default=attr.Factory(dict))
|
||||
uploader = attr.ib(default=None)
|
||||
|
||||
@classmethod
|
||||
def new(cls, reporter, kind):
|
||||
base = "{:.9f}.{}".format(time.time(), kind.name.lower())
|
||||
crash_file = open(
|
||||
os.path.join(reporter.crash_directory, base + ".crash"),
|
||||
'wb')
|
||||
|
||||
pr = apport.Report('Bug')
|
||||
pr['CrashDB'] = repr(reporter.crashdb_spec)
|
||||
|
||||
r = cls(
|
||||
reporter=reporter, base=base, pr=pr, file=crash_file,
|
||||
state=ErrorReportState.INCOMPLETE,
|
||||
context=reporter.context.child(base))
|
||||
r.set_meta("kind", kind.name)
|
||||
return r
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, reporter, fpath):
|
||||
base = os.path.splitext(os.path.basename(fpath))[0]
|
||||
report = cls(
|
||||
reporter, base, pr=apport.Report(date='???'),
|
||||
state=ErrorReportState.LOADING, file=open(fpath, 'rb'),
|
||||
context=reporter.context.child(base))
|
||||
try:
|
||||
fp = open(report.meta_path, 'r')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
with fp:
|
||||
report.meta = json.load(fp)
|
||||
return report
|
||||
|
||||
def add_info(self, _bg_attach_hook, wait=False):
|
||||
def _bg_add_info():
|
||||
_bg_attach_hook()
|
||||
# Add basic info to report.
|
||||
self.pr.add_proc_info()
|
||||
self.pr.add_os_info()
|
||||
self.pr.add_hooks_info(None)
|
||||
apport.hookutils.attach_hardware(self.pr)
|
||||
# Because apport-cli will in general be run on a different
|
||||
# machine, we make some slightly obscure alterations to the report
|
||||
# to make this go better.
|
||||
|
||||
# apport-cli gets upset if neither of these are present.
|
||||
self.pr['Package'] = 'subiquity ' + os.environ.get(
|
||||
"SNAP_REVISION", "SNAP_REVISION")
|
||||
self.pr['SourcePackage'] = 'subiquity'
|
||||
|
||||
# If ExecutableTimestamp is present, apport-cli will try to check
|
||||
# that ExecutablePath hasn't changed. But it won't be there.
|
||||
del self.pr['ExecutableTimestamp']
|
||||
# apport-cli gets upset at the probert C extensions it sees in
|
||||
# here. /proc/maps is very unlikely to be interesting for us
|
||||
# anyway.
|
||||
del self.pr['ProcMaps']
|
||||
self.pr.write(self._file)
|
||||
|
||||
async def add_info():
|
||||
with self._context.child("add_info") as context:
|
||||
try:
|
||||
await run_in_thread(_bg_add_info)
|
||||
except Exception:
|
||||
self.state = ErrorReportState.ERROR_GENERATING
|
||||
log.exception("adding info to problem report failed")
|
||||
else:
|
||||
context.description = "written to " + self.path
|
||||
self.state = ErrorReportState.DONE
|
||||
self._file.close()
|
||||
self._file = None
|
||||
urwid.emit_signal(self, "changed")
|
||||
if wait:
|
||||
with self._context.child("add_info") as context:
|
||||
_bg_add_info()
|
||||
context.description = "written to " + self.path
|
||||
else:
|
||||
schedule_task(add_info())
|
||||
|
||||
async def load(self):
|
||||
with self._context.child("load"):
|
||||
# Load report from disk in background.
|
||||
try:
|
||||
await run_in_thread(self.pr.load, self._file)
|
||||
except Exception:
|
||||
log.exception("loading problem report failed")
|
||||
self.state = ErrorReportState.ERROR_LOADING
|
||||
else:
|
||||
self.state = ErrorReportState.DONE
|
||||
self._file.close()
|
||||
self._file = None
|
||||
urwid.emit_signal(self, "changed")
|
||||
|
||||
def upload(self):
|
||||
uploader = self.uploader = Upload(bytes_to_send=1)
|
||||
|
||||
url = "https://daisy.ubuntu.com"
|
||||
if self.reporter.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 {
|
||||
"InstallerLogInfo",
|
||||
"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]
|
||||
|
||||
async def upload():
|
||||
with self._context.child("upload") as context:
|
||||
try:
|
||||
oops_id = await run_in_thread(_bg_upload)
|
||||
except requests.exceptions.RequestException:
|
||||
log.exception("upload for %s failed", self.base)
|
||||
else:
|
||||
self.set_meta("oops-id", oops_id)
|
||||
context.description = oops_id
|
||||
uploader.stop()
|
||||
self.uploader = None
|
||||
urwid.emit_signal(self, 'changed')
|
||||
|
||||
urwid.emit_signal(self, 'changed')
|
||||
uploader.start()
|
||||
|
||||
schedule_task(upload())
|
||||
|
||||
def _path_with_ext(self, ext):
|
||||
return os.path.join(
|
||||
self.reporter.crash_directory, self.base + '.' + ext)
|
||||
|
||||
@property
|
||||
def meta_path(self):
|
||||
return self._path_with_ext('meta')
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path_with_ext('crash')
|
||||
|
||||
def set_meta(self, key, value):
|
||||
self.meta[key] = value
|
||||
with open(self.meta_path, 'w') as fp:
|
||||
json.dump(self.meta, fp, indent=4)
|
||||
|
||||
def mark_seen(self):
|
||||
self.set_meta("seen", True)
|
||||
urwid.emit_signal(self, "changed")
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
k = self.meta.get("kind", "UNKNOWN")
|
||||
return getattr(ErrorReportKind, k, ErrorReportKind.UNKNOWN)
|
||||
|
||||
@property
|
||||
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."""
|
||||
# Not sure if this is more or less sane than shelling out to
|
||||
# findmnt(1).
|
||||
looking_for = os.path.abspath(
|
||||
os.path.normpath(self.reporter.crash_directory))
|
||||
for line in open('/proc/self/mountinfo').readlines():
|
||||
parts = line.strip().split()
|
||||
if os.path.normpath(parts[4]) == looking_for:
|
||||
devname = parts[9]
|
||||
root = parts[3]
|
||||
break
|
||||
else:
|
||||
if self.reporter.dry_run:
|
||||
path = ('install-logs/2019-11-06.0/crash/' +
|
||||
self.base +
|
||||
'.crash')
|
||||
return "casper-rw", path
|
||||
return None, None
|
||||
import pyudev
|
||||
c = pyudev.Context()
|
||||
devs = list(c.list_devices(
|
||||
subsystem='block', DEVNAME=os.path.realpath(devname)))
|
||||
if not devs:
|
||||
return None, None
|
||||
label = devs[0].get('ID_FS_LABEL_ENC', '')
|
||||
return label, root[1:] + '/' + self.base + '.crash'
|
||||
|
||||
|
||||
class ErrorReporter(object):
|
||||
|
||||
def __init__(self, context, dry_run, root):
|
||||
self.context = context
|
||||
self.dry_run = dry_run
|
||||
self.reports = []
|
||||
if dry_run:
|
||||
self.crash_directory = os.path.join(root, 'var/crash')
|
||||
self.crashdb_spec = {
|
||||
'impl': 'launchpad',
|
||||
'project': 'subiquity',
|
||||
}
|
||||
if dry_run:
|
||||
self.crashdb_spec['launchpad_instance'] = 'staging'
|
||||
self._apport_data = []
|
||||
self._apport_files = []
|
||||
|
||||
def start_loading_reports(self):
|
||||
os.makedirs(self.crash_directory, exist_ok=True)
|
||||
filenames = os.listdir(self.crash_directory)
|
||||
to_load = []
|
||||
for filename in sorted(filenames, reverse=True):
|
||||
base, ext = os.path.splitext(filename)
|
||||
if ext != ".crash":
|
||||
continue
|
||||
path = os.path.join(self.crash_directory, filename)
|
||||
r = ErrorReport.from_file(self, path)
|
||||
self.reports.append(r)
|
||||
to_load.append(r)
|
||||
schedule_task(self._load_reports(to_load))
|
||||
|
||||
async def _load_reports(self, to_load):
|
||||
for report in to_load:
|
||||
await report.load()
|
||||
|
||||
def note_file_for_apport(self, key, path):
|
||||
self._apport_files.append((key, path))
|
||||
|
||||
def note_data_for_apport(self, key, value):
|
||||
self._apport_data.append((key, value))
|
||||
|
||||
def make_apport_report(self, kind, thing, *, wait=False, **kw):
|
||||
if not self.dry_run and not os.path.exists('/cdrom/.disk/info'):
|
||||
return None
|
||||
|
||||
log.debug("generating crash report")
|
||||
|
||||
try:
|
||||
report = ErrorReport.new(self, kind)
|
||||
self.reports.insert(0, report)
|
||||
except Exception:
|
||||
log.exception("creating crash report failed")
|
||||
return
|
||||
|
||||
etype = sys.exc_info()[0]
|
||||
if etype is not None:
|
||||
report.pr["Title"] = "{} crashed with {}".format(
|
||||
thing, etype.__name__)
|
||||
report.pr['Traceback'] = traceback.format_exc()
|
||||
else:
|
||||
report.pr["Title"] = thing
|
||||
|
||||
log.info(
|
||||
"saving crash report %r to %s", report.pr["Title"], report.path)
|
||||
|
||||
apport_files = self._apport_files[:]
|
||||
apport_data = self._apport_data.copy()
|
||||
|
||||
def _bg_attach_hook():
|
||||
# Attach any stuff other parts of the code think we should know
|
||||
# about.
|
||||
for key, path in apport_files:
|
||||
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)
|
||||
|
||||
# In the fullness of time we should do the signature thing here.
|
||||
return report
|
|
@ -13,28 +13,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import enum
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import apport
|
||||
import apport.crashdb
|
||||
import apport.hookutils
|
||||
|
||||
import bson
|
||||
|
||||
import attr
|
||||
|
||||
import requests
|
||||
|
||||
import urwid
|
||||
|
||||
from subiquitycore.async_helpers import (
|
||||
run_in_thread,
|
||||
schedule_task,
|
||||
)
|
||||
|
||||
from subiquity.controllers.cmdlist import CmdListController
|
||||
|
||||
|
@ -42,328 +21,7 @@ from subiquity.controllers.cmdlist import CmdListController
|
|||
log = logging.getLogger('subiquity.controllers.error')
|
||||
|
||||
|
||||
class ErrorReportState(enum.Enum):
|
||||
INCOMPLETE = enum.auto()
|
||||
LOADING = enum.auto()
|
||||
DONE = enum.auto()
|
||||
ERROR_GENERATING = enum.auto()
|
||||
ERROR_LOADING = enum.auto()
|
||||
|
||||
|
||||
class ErrorReportKind(enum.Enum):
|
||||
BLOCK_PROBE_FAIL = _("Block device probe failure")
|
||||
DISK_PROBE_FAIL = _("Disk probe failure")
|
||||
INSTALL_FAIL = _("Install failure")
|
||||
UI = _("Installer crash")
|
||||
NETWORK_FAIL = _("Network error")
|
||||
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.app.urwid_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.app.urwid_loop.remove_watch_pipe(self.pipe_w)
|
||||
os.close(self.pipe_w)
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class ErrorReport(metaclass=urwid.MetaSignals):
|
||||
|
||||
signals = ["changed"]
|
||||
|
||||
controller = attr.ib()
|
||||
base = attr.ib()
|
||||
pr = attr.ib()
|
||||
state = attr.ib()
|
||||
_file = attr.ib()
|
||||
_context = attr.ib()
|
||||
|
||||
meta = attr.ib(default=attr.Factory(dict))
|
||||
uploader = attr.ib(default=None)
|
||||
|
||||
@classmethod
|
||||
def new(cls, controller, kind):
|
||||
base = "{:.9f}.{}".format(time.time(), kind.name.lower())
|
||||
crash_file = open(
|
||||
os.path.join(controller.crash_directory, base + ".crash"),
|
||||
'wb')
|
||||
|
||||
pr = apport.Report('Bug')
|
||||
pr['CrashDB'] = repr(controller.crashdb_spec)
|
||||
|
||||
r = cls(
|
||||
controller=controller, base=base, pr=pr, file=crash_file,
|
||||
state=ErrorReportState.INCOMPLETE,
|
||||
context=controller.context.child(base))
|
||||
r.set_meta("kind", kind.name)
|
||||
return r
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, controller, fpath):
|
||||
base = os.path.splitext(os.path.basename(fpath))[0]
|
||||
report = cls(
|
||||
controller, base, pr=apport.Report(date='???'),
|
||||
state=ErrorReportState.LOADING, file=open(fpath, 'rb'),
|
||||
context=controller.context.child(base))
|
||||
try:
|
||||
fp = open(report.meta_path, 'r')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
with fp:
|
||||
report.meta = json.load(fp)
|
||||
return report
|
||||
|
||||
def add_info(self, _bg_attach_hook, wait=False):
|
||||
def _bg_add_info():
|
||||
_bg_attach_hook()
|
||||
# Add basic info to report.
|
||||
self.pr.add_proc_info()
|
||||
self.pr.add_os_info()
|
||||
self.pr.add_hooks_info(None)
|
||||
apport.hookutils.attach_hardware(self.pr)
|
||||
# Because apport-cli will in general be run on a different
|
||||
# machine, we make some slightly obscure alterations to the report
|
||||
# to make this go better.
|
||||
|
||||
# apport-cli gets upset if neither of these are present.
|
||||
self.pr['Package'] = 'subiquity ' + os.environ.get(
|
||||
"SNAP_REVISION", "SNAP_REVISION")
|
||||
self.pr['SourcePackage'] = 'subiquity'
|
||||
|
||||
# If ExecutableTimestamp is present, apport-cli will try to check
|
||||
# that ExecutablePath hasn't changed. But it won't be there.
|
||||
del self.pr['ExecutableTimestamp']
|
||||
# apport-cli gets upset at the probert C extensions it sees in
|
||||
# here. /proc/maps is very unlikely to be interesting for us
|
||||
# anyway.
|
||||
del self.pr['ProcMaps']
|
||||
self.pr.write(self._file)
|
||||
|
||||
async def add_info():
|
||||
with self._context.child("add_info") as context:
|
||||
try:
|
||||
await run_in_thread(_bg_add_info)
|
||||
except Exception:
|
||||
self.state = ErrorReportState.ERROR_GENERATING
|
||||
log.exception("adding info to problem report failed")
|
||||
else:
|
||||
context.description = "written to " + self.path
|
||||
self.state = ErrorReportState.DONE
|
||||
self._file.close()
|
||||
self._file = None
|
||||
urwid.emit_signal(self, "changed")
|
||||
if wait:
|
||||
with self._context.child("add_info") as context:
|
||||
_bg_add_info()
|
||||
context.description = "written to " + self.path
|
||||
else:
|
||||
schedule_task(add_info())
|
||||
|
||||
async def load(self):
|
||||
with self._context.child("load"):
|
||||
# Load report from disk in background.
|
||||
try:
|
||||
await run_in_thread(self.pr.load, self._file)
|
||||
except Exception:
|
||||
log.exception("loading problem report failed")
|
||||
self.state = ErrorReportState.ERROR_LOADING
|
||||
else:
|
||||
self.state = ErrorReportState.DONE
|
||||
self._file.close()
|
||||
self._file = None
|
||||
urwid.emit_signal(self, "changed")
|
||||
|
||||
def upload(self):
|
||||
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 {
|
||||
"InstallerLogInfo",
|
||||
"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]
|
||||
|
||||
async def upload():
|
||||
with self._context.child("upload") as context:
|
||||
try:
|
||||
oops_id = await run_in_thread(_bg_upload)
|
||||
except requests.exceptions.RequestException:
|
||||
log.exception("upload for %s failed", self.base)
|
||||
else:
|
||||
self.set_meta("oops-id", oops_id)
|
||||
context.description = oops_id
|
||||
uploader.stop()
|
||||
self.uploader = None
|
||||
urwid.emit_signal(self, 'changed')
|
||||
|
||||
urwid.emit_signal(self, 'changed')
|
||||
uploader.start()
|
||||
|
||||
schedule_task(upload())
|
||||
|
||||
def _path_with_ext(self, ext):
|
||||
return os.path.join(
|
||||
self.controller.crash_directory, self.base + '.' + ext)
|
||||
|
||||
@property
|
||||
def meta_path(self):
|
||||
return self._path_with_ext('meta')
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path_with_ext('crash')
|
||||
|
||||
def set_meta(self, key, value):
|
||||
self.meta[key] = value
|
||||
with open(self.meta_path, 'w') as fp:
|
||||
json.dump(self.meta, fp, indent=4)
|
||||
|
||||
def mark_seen(self):
|
||||
self.set_meta("seen", True)
|
||||
urwid.emit_signal(self, "changed")
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
k = self.meta.get("kind", "UNKNOWN")
|
||||
return getattr(ErrorReportKind, k, ErrorReportKind.UNKNOWN)
|
||||
|
||||
@property
|
||||
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."""
|
||||
# Not sure if this is more or less sane than shelling out to
|
||||
# findmnt(1).
|
||||
looking_for = os.path.abspath(
|
||||
os.path.normpath(self.controller.crash_directory))
|
||||
for line in open('/proc/self/mountinfo').readlines():
|
||||
parts = line.strip().split()
|
||||
if os.path.normpath(parts[4]) == looking_for:
|
||||
devname = parts[9]
|
||||
root = parts[3]
|
||||
break
|
||||
else:
|
||||
if self.controller.opts.dry_run:
|
||||
path = ('install-logs/2019-11-06.0/crash/' +
|
||||
self.base +
|
||||
'.crash')
|
||||
return "casper-rw", path
|
||||
return None, None
|
||||
import pyudev
|
||||
c = pyudev.Context()
|
||||
devs = list(c.list_devices(
|
||||
subsystem='block', DEVNAME=os.path.realpath(devname)))
|
||||
if not devs:
|
||||
return None, None
|
||||
label = devs[0].get('ID_FS_LABEL_ENC', '')
|
||||
return label, root[1:] + '/' + self.base + '.crash'
|
||||
|
||||
|
||||
class ErrorController(CmdListController):
|
||||
|
||||
autoinstall_key = 'error-commands'
|
||||
cmd_check = False
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self.crash_directory = os.path.join(self.app.root, 'var/crash')
|
||||
self.crashdb_spec = {
|
||||
'impl': 'launchpad',
|
||||
'project': 'subiquity',
|
||||
}
|
||||
if self.app.opts.dry_run:
|
||||
self.crashdb_spec['launchpad_instance'] = 'staging'
|
||||
self.reports = []
|
||||
|
||||
def start(self):
|
||||
os.makedirs(self.crash_directory, exist_ok=True)
|
||||
# scan for pre-existing crash reports and start loading them
|
||||
# in the background
|
||||
self.scan_crash_dir()
|
||||
|
||||
async def _load_reports(self, to_load):
|
||||
for report in to_load:
|
||||
await report.load()
|
||||
|
||||
def scan_crash_dir(self):
|
||||
filenames = os.listdir(self.crash_directory)
|
||||
to_load = []
|
||||
for filename in sorted(filenames, reverse=True):
|
||||
base, ext = os.path.splitext(filename)
|
||||
if ext != ".crash":
|
||||
continue
|
||||
path = os.path.join(self.crash_directory, filename)
|
||||
r = ErrorReport.from_file(self, path)
|
||||
self.reports.append(r)
|
||||
to_load.append(r)
|
||||
schedule_task(self._load_reports(to_load))
|
||||
|
||||
def create_report(self, kind):
|
||||
r = ErrorReport.new(self, kind)
|
||||
self.reports.insert(0, r)
|
||||
return r
|
||||
|
|
|
@ -33,8 +33,8 @@ from subiquitycore.utils import (
|
|||
)
|
||||
|
||||
|
||||
from subiquity.common.errorreport import ErrorReportKind
|
||||
from subiquity.controller import SubiquityController
|
||||
from subiquity.controllers.error import ErrorReportKind
|
||||
from subiquity.models.filesystem import (
|
||||
align_up,
|
||||
Bootloader,
|
||||
|
|
|
@ -43,8 +43,8 @@ from subiquitycore.utils import (
|
|||
astart_command,
|
||||
)
|
||||
|
||||
from subiquity.common.errorreport import ErrorReportKind
|
||||
from subiquity.controller import SubiquityController
|
||||
from subiquity.controllers.error import ErrorReportKind
|
||||
from subiquity.journald import journald_listener
|
||||
from subiquity.ui.views.installprogress import ProgressView
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ from subiquitycore.async_helpers import schedule_task
|
|||
from subiquitycore.context import with_context
|
||||
from subiquitycore.controllers.network import NetworkController
|
||||
|
||||
from subiquity.common.errorreport import ErrorReportKind
|
||||
from subiquity.controller import SubiquityController
|
||||
from subiquity.controllers.error import ErrorReportKind
|
||||
|
||||
|
||||
log = logging.getLogger("subiquity.controllers.network")
|
||||
|
|
|
@ -23,8 +23,6 @@ import traceback
|
|||
import time
|
||||
import urwid
|
||||
|
||||
import apport.hookutils
|
||||
|
||||
import jsonschema
|
||||
|
||||
import yaml
|
||||
|
@ -42,7 +40,8 @@ from subiquitycore.snapd import (
|
|||
)
|
||||
from subiquitycore.view import BaseView
|
||||
|
||||
from subiquity.controllers.error import (
|
||||
from subiquity.common.errorreport import (
|
||||
ErrorReporter,
|
||||
ErrorReportKind,
|
||||
)
|
||||
from subiquity.journald import journald_listener
|
||||
|
@ -149,14 +148,15 @@ class Subiquity(Application):
|
|||
('network-proxy-set', lambda: schedule_task(self._proxy_set())),
|
||||
('network-change', self._network_change),
|
||||
])
|
||||
self._apport_data = []
|
||||
self._apport_files = []
|
||||
|
||||
self.autoinstall_config = {}
|
||||
self.report_to_show = None
|
||||
self.show_progress_handle = None
|
||||
self.progress_shown_time = self.aio_loop.time()
|
||||
self.progress_showing = False
|
||||
self.error_reporter = ErrorReporter(
|
||||
self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
|
||||
|
||||
self.note_data_for_apport("SnapUpdated", str(self.updated))
|
||||
self.note_data_for_apport("UsingAnswers", str(bool(self.answers)))
|
||||
|
||||
|
@ -365,7 +365,8 @@ class Subiquity(Application):
|
|||
self.ui.body.remove_overlay(overlay)
|
||||
|
||||
def select_initial_screen(self, index):
|
||||
for report in self.controllers.Error.reports:
|
||||
self.error_reporter.start_loading_reports()
|
||||
for report in self.error_reporter.reports:
|
||||
if report.kind == ErrorReportKind.UI and not report.seen:
|
||||
self.show_error_report(report)
|
||||
break
|
||||
|
@ -458,53 +459,18 @@ class Subiquity(Application):
|
|||
["bash"], before_hook=_before, after_hook=after_hook, cwd='/')
|
||||
|
||||
def note_file_for_apport(self, key, path):
|
||||
self._apport_files.append((key, path))
|
||||
self.error_reporter.note_file_for_apport(key, path)
|
||||
|
||||
def note_data_for_apport(self, key, value):
|
||||
self._apport_data.append((key, value))
|
||||
self.error_reporter.note_data_for_apport(key, value)
|
||||
|
||||
def make_apport_report(self, kind, thing, *, interrupt, wait=False, **kw):
|
||||
if not self.opts.dry_run and not os.path.exists('/cdrom/.disk/info'):
|
||||
return None
|
||||
report = self.error_reporter.make_apport_report(
|
||||
kind, thing, wait=wait, **kw)
|
||||
|
||||
log.debug("generating crash report")
|
||||
|
||||
try:
|
||||
report = self.controllers.Error.create_report(kind)
|
||||
except Exception:
|
||||
log.exception("creating crash report failed")
|
||||
return
|
||||
|
||||
etype = sys.exc_info()[0]
|
||||
if etype is not None:
|
||||
report.pr["Title"] = "{} crashed with {}".format(
|
||||
thing, etype.__name__)
|
||||
report.pr['Traceback'] = traceback.format_exc()
|
||||
else:
|
||||
report.pr["Title"] = thing
|
||||
|
||||
log.info(
|
||||
"saving crash report %r to %s", report.pr["Title"], report.path)
|
||||
|
||||
apport_files = self._apport_files[:]
|
||||
apport_data = self._apport_data.copy()
|
||||
|
||||
def _bg_attach_hook():
|
||||
# Attach any stuff other parts of the code think we should know
|
||||
# about.
|
||||
for key, path in apport_files:
|
||||
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)
|
||||
|
||||
if interrupt and self.interactive():
|
||||
if report is not None and interrupt and self.interactive():
|
||||
self.show_error_report(report)
|
||||
|
||||
# In the fullness of time we should do the signature thing here.
|
||||
return report
|
||||
|
||||
def show_error_report(self, report):
|
||||
|
|
|
@ -45,7 +45,7 @@ from subiquitycore.ui.width import (
|
|||
widget_width,
|
||||
)
|
||||
|
||||
from subiquity.controllers.error import (
|
||||
from subiquity.common.errorreport import (
|
||||
ErrorReportKind,
|
||||
ErrorReportState,
|
||||
)
|
||||
|
@ -285,7 +285,7 @@ class ErrorReportListStretchy(Stretchy):
|
|||
Text(""),
|
||||
])]
|
||||
self.report_to_row = {}
|
||||
for report in self.app.controllers.Error.reports:
|
||||
for report in self.app.error_reporter.reports:
|
||||
connect_signal(report, "changed", self._report_changed, report)
|
||||
r = self.report_to_row[report] = self.row_for_report(report)
|
||||
rows.append(r)
|
||||
|
|
|
@ -302,7 +302,7 @@ class OpenHelpMenu(WidgetWrap):
|
|||
local = Text(
|
||||
('info_minor header', " " + _("Help on this screen") + " "))
|
||||
|
||||
if self.parent.app.controllers.Error.reports:
|
||||
if self.parent.app.error_reporter.reports:
|
||||
view_errors = menu_item(
|
||||
_("View error reports").format(local_title),
|
||||
on_press=self.parent.show_errors)
|
||||
|
|
Loading…
Reference in New Issue