Merge pull request #1888 from ogayot/apport-title-show-curtin-step
apport: show name of curtin stage when an error occurs
This commit is contained in:
commit
d7e84a176f
|
@ -55,6 +55,12 @@ from subiquitycore.utils import arun_command, log_process_streams
|
||||||
log = logging.getLogger("subiquity.server.controllers.install")
|
log = logging.getLogger("subiquity.server.controllers.install")
|
||||||
|
|
||||||
|
|
||||||
|
class CurtinInstallError(Exception):
|
||||||
|
def __init__(self, *, stages: List[str]) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.stages = stages
|
||||||
|
|
||||||
|
|
||||||
class TracebackExtractor:
|
class TracebackExtractor:
|
||||||
start_marker = re.compile(r"^Traceback \(most recent call last\):")
|
start_marker = re.compile(r"^Traceback \(most recent call last\):")
|
||||||
end_marker = re.compile(r"\S")
|
end_marker = re.compile(r"\S")
|
||||||
|
@ -195,6 +201,17 @@ class InstallController(SubiquityController):
|
||||||
keyboard = self.app.controllers.Keyboard
|
keyboard = self.app.controllers.Keyboard
|
||||||
await keyboard.setup_target(context=context)
|
await keyboard.setup_target(context=context)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def error_in_curtin_invocation(exc: Exception) -> Optional[str]:
|
||||||
|
"""If the exception passed as an argument corresponds to an error
|
||||||
|
during the invocation of a single curtin stage, return the name of the
|
||||||
|
stage. Otherwise, return None."""
|
||||||
|
if not isinstance(exc, CurtinInstallError):
|
||||||
|
return None
|
||||||
|
if len(exc.stages) != 1:
|
||||||
|
return None
|
||||||
|
return exc.stages[0]
|
||||||
|
|
||||||
@with_context(description="executing curtin install {name} step")
|
@with_context(description="executing curtin install {name} step")
|
||||||
async def run_curtin_step(
|
async def run_curtin_step(
|
||||||
self,
|
self,
|
||||||
|
@ -226,16 +243,19 @@ class InstallController(SubiquityController):
|
||||||
else:
|
else:
|
||||||
source_args = ()
|
source_args = ()
|
||||||
|
|
||||||
await run_curtin_command(
|
try:
|
||||||
self.app,
|
await run_curtin_command(
|
||||||
context,
|
self.app,
|
||||||
"install",
|
context,
|
||||||
"--set",
|
"install",
|
||||||
f"json:stages={json.dumps(stages)}",
|
"--set",
|
||||||
*source_args,
|
f"json:stages={json.dumps(stages)}",
|
||||||
config=str(config_file),
|
*source_args,
|
||||||
private_mounts=False,
|
config=str(config_file),
|
||||||
)
|
private_mounts=False,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
raise CurtinInstallError(stages=stages)
|
||||||
|
|
||||||
device_map_path = config.get("storage", {}).get("device_map_path")
|
device_map_path = config.get("storage", {}).get("device_map_path")
|
||||||
if device_map_path is not None:
|
if device_map_path is not None:
|
||||||
|
@ -673,13 +693,13 @@ class InstallController(SubiquityController):
|
||||||
await self.postinstall(context=context)
|
await self.postinstall(context=context)
|
||||||
|
|
||||||
self.app.update_state(ApplicationState.DONE)
|
self.app.update_state(ApplicationState.DONE)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
kw = {}
|
kw = {}
|
||||||
if self.tb_extractor.traceback:
|
if self.tb_extractor.traceback:
|
||||||
kw["Traceback"] = "\n".join(self.tb_extractor.traceback)
|
kw["Traceback"] = "\n".join(self.tb_extractor.traceback)
|
||||||
self.app.make_apport_report(
|
text = self.error_in_curtin_invocation(exc) or "install failed"
|
||||||
ErrorReportKind.INSTALL_FAIL, "install failed", **kw
|
|
||||||
)
|
self.app.make_apport_report(ErrorReportKind.INSTALL_FAIL, text, **kw)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def platform_postinstall(self):
|
async def platform_postinstall(self):
|
||||||
|
|
|
@ -25,7 +25,7 @@ from curtin.util import EFIBootEntry, EFIBootState
|
||||||
|
|
||||||
from subiquity.common.types import PackageInstallState
|
from subiquity.common.types import PackageInstallState
|
||||||
from subiquity.models.tests.test_filesystem import make_model_and_partition
|
from subiquity.models.tests.test_filesystem import make_model_and_partition
|
||||||
from subiquity.server.controllers.install import InstallController
|
from subiquity.server.controllers.install import CurtinInstallError, InstallController
|
||||||
from subiquitycore.tests.mocks import make_app
|
from subiquitycore.tests.mocks import make_app
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,6 +88,31 @@ class TestWriteConfig(unittest.IsolatedAsyncioTestCase):
|
||||||
private_mounts=False,
|
private_mounts=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch("subiquity.server.controllers.install.open", mock_open())
|
||||||
|
async def test_run_curtin_install_step_failed(self):
|
||||||
|
cmd = ["curtin", "install", "--set", 'json:stages=["partitioning"]']
|
||||||
|
stages = ["partitioning"]
|
||||||
|
|
||||||
|
async def fake_run_curtin_command(*args, **kwargs):
|
||||||
|
raise subprocess.CalledProcessError(returncode=1, cmd=cmd)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"subiquity.server.controllers.install.run_curtin_command",
|
||||||
|
fake_run_curtin_command,
|
||||||
|
):
|
||||||
|
with self.assertRaises(CurtinInstallError) as exc_cm:
|
||||||
|
await self.controller.run_curtin_step(
|
||||||
|
name="MyStep",
|
||||||
|
stages=stages,
|
||||||
|
config_file=Path("/config.yaml"),
|
||||||
|
source=None,
|
||||||
|
config=self.controller.base_config(
|
||||||
|
logs_dir=Path("/"), resume_data_file=Path("resume-data")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(stages, exc_cm.exception.stages)
|
||||||
|
self.assertEqual(cmd, exc_cm.exception.__context__.cmd)
|
||||||
|
|
||||||
def test_base_config(self):
|
def test_base_config(self):
|
||||||
config = self.controller.base_config(
|
config = self.controller.base_config(
|
||||||
logs_dir=Path("/logs"), resume_data_file=Path("resume-data")
|
logs_dir=Path("/logs"), resume_data_file=Path("resume-data")
|
||||||
|
@ -397,3 +422,19 @@ class TestInstallController(unittest.IsolatedAsyncioTestCase):
|
||||||
"https://canonical-subiquity.readthedocs-hosted.com/en/latest/reference/autoinstall-reference.html", # noqa: E501
|
"https://canonical-subiquity.readthedocs-hosted.com/en/latest/reference/autoinstall-reference.html", # noqa: E501
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_error_in_curtin_invocation(self):
|
||||||
|
method = self.controller.error_in_curtin_invocation
|
||||||
|
|
||||||
|
self.assertIsNone(method(Exception()))
|
||||||
|
self.assertIsNone(method(RuntimeError()))
|
||||||
|
|
||||||
|
self.assertIsNone(method(CurtinInstallError(stages=[])))
|
||||||
|
# Running multiple stages in one "curtin step" is not something that
|
||||||
|
# currently happens in practice.
|
||||||
|
self.assertIsNone(
|
||||||
|
method(CurtinInstallError(stages=["extract", "partitioning"]))
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("extract", method(CurtinInstallError(stages=["extract"])))
|
||||||
|
self.assertEqual("curthooks", method(CurtinInstallError(stages=["curthooks"])))
|
||||||
|
|
Loading…
Reference in New Issue