api: add exception endpoint

Create a new api endpoint called exception, that communicates
exception backtraces separately back to osbuild, as opposed to
dumping them into the normal log. Additionally, add a corresponding
test to check that a call to api.exception correctly sets
API.exception.
This commit is contained in:
Chloe Kaubisch 2020-09-29 13:04:46 +02:00
parent 661e202e79
commit 5dc5ddcf29
3 changed files with 55 additions and 3 deletions

View file

@ -1,9 +1,12 @@
import abc
import asyncio
import contextlib
import io
import json
import os
import sys
import tempfile
import traceback
import threading
from typing import Dict, Optional
from .util.types import PathLike
@ -141,6 +144,7 @@ class API(BaseAPI):
self._output_pipe = None
self.monitor = monitor
self.metadata = {}
self.exception = None
@property
def output(self):
@ -182,9 +186,14 @@ class API(BaseAPI):
fds.append(data.fileno())
sock.send({"type": "fd", "fd": 0}, fds=fds)
def _get_exception(self, message):
self.exception = message["exception"]
def _message(self, msg, fds, sock):
if msg["method"] == 'add-metadata':
self._set_metadata(msg)
elif msg["method"] == 'exception':
self._get_exception(msg)
elif msg["method"] == 'get-arguments':
self._get_arguments(sock)
@ -193,6 +202,29 @@ class API(BaseAPI):
os.close(self._output_pipe)
self._output_pipe = None
def exception(e, path="/run/osbuild/api/osbuild"):
"""Send exception to osbuild"""
traceback.print_exception(type(e), e, e.__traceback__, file=sys.stderr)
with jsoncomm.Socket.new_client(path) as client:
msg = {
"method": "exception",
"exception": {
"type": str(type(e)),
"value": str(e),
"traceback": str(e.__traceback__)
}
}
client.send(msg)
sys.exit(2)
# pylint: disable=broad-except
@contextlib.contextmanager
def exception_handler(path="/run/osbuild/api/osbuild"):
try:
yield
except Exception as e:
exception(e, path)
def arguments(path="/run/osbuild/api/osbuild"):
"""Retrieve the input arguments that were supplied to API"""

View file

@ -18,13 +18,14 @@ def cleanup(*objs):
class BuildResult:
def __init__(self, origin, returncode, output, metadata):
def __init__(self, origin, returncode, output, metadata, error):
self.name = origin.name
self.id = origin.id
self.options = origin.options
self.success = returncode == 0
self.output = output
self.metadata = metadata
self.error = error
def as_dict(self):
return vars(self)
@ -92,7 +93,7 @@ class Stage:
binds=[os.fspath(tree) + ":/run/osbuild/tree"],
readonly_binds=ro_binds)
return BuildResult(self, r.returncode, r.output, api.metadata)
return BuildResult(self, r.returncode, r.output, api.metadata, api.exception)
class Assembler:
@ -151,7 +152,7 @@ class Assembler:
binds=binds,
readonly_binds=ro_binds)
return BuildResult(self, r.returncode, r.output, api.metadata)
return BuildResult(self, r.returncode, r.output, api.metadata, api.exception)
class Pipeline:

View file

@ -79,6 +79,25 @@ class TestAPI(unittest.TestCase):
self.assertEqual(data, args)
def test_exception(self):
# Check that 'api.exception' correctly sets 'API.exception'
tmpdir = self.tmp.name
path = os.path.join(tmpdir, "osbuild-api")
args = {}
monitor = osbuild.monitor.BaseMonitor(sys.stderr.fileno())
def exception(path):
with osbuild.api.exception_handler(path):
raise ValueError("osbuild test exception")
api = osbuild.api.API(args, monitor, socket_address=path)
with api:
p = mp.Process(target=exception, args=(path, ))
p.start()
p.join()
self.assertIsNotNone(api.exception, "Exception not set")
self.assertEqual(api.exception["value"], "osbuild test exception")
def test_metadata(self):
# Check that `api.metadata` leads to `API.metadata` being
# set correctly