From cd8667d6a1e8203ea0e971e2a4ce0af13ebb3920 Mon Sep 17 00:00:00 2001 From: Dan Bungert Date: Mon, 7 Nov 2022 17:51:53 -0700 Subject: [PATCH] integrity: add md5 check integration * log the results of the md5 check * if a failure and a crash occurs, mention that in the crash dialog --- subiquity/common/apidef.py | 4 + subiquity/common/types.py | 7 ++ subiquity/models/integrity.py | 18 ++++ subiquity/models/subiquity.py | 2 + subiquity/server/controllers/__init__.py | 2 + subiquity/server/controllers/integrity.py | 83 +++++++++++++++++++ .../controllers/tests/test_integrity.py | 49 +++++++++++ subiquity/server/server.py | 1 + subiquity/ui/views/error.py | 14 ++++ 9 files changed, 180 insertions(+) create mode 100644 subiquity/models/integrity.py create mode 100644 subiquity/server/controllers/integrity.py create mode 100644 subiquity/server/controllers/tests/test_integrity.py diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index f47ab1db..86cc8dba 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -29,6 +29,7 @@ from subiquity.common.types import ( AnyStep, ApplicationState, ApplicationStatus, + CasperMd5Results, Change, Disk, ErrorReportRef, @@ -382,6 +383,9 @@ class API: class fetch_id: def GET(user_id: str) -> SSHFetchIdResponse: ... + class integrity: + def GET() -> CasperMd5Results: ... + class LinkAction(enum.Enum): NEW = enum.auto() diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 9c66aca0..e7a04205 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -725,3 +725,10 @@ class Change: ready: bool err: Optional[str] = None data: Any = None + + +class CasperMd5Results(enum.Enum): + UNKNOWN = 'unknown' + FAIL = 'fail' + PASS = 'pass' + SKIP = 'skip' diff --git a/subiquity/models/integrity.py b/subiquity/models/integrity.py new file mode 100644 index 00000000..3115dfd1 --- /dev/null +++ b/subiquity/models/integrity.py @@ -0,0 +1,18 @@ +# Copyright 2022 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 . + + +class IntegrityModel: + md5check_results = {} diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index bd0dccb6..42abf9bc 100644 --- a/subiquity/models/subiquity.py +++ b/subiquity/models/subiquity.py @@ -50,6 +50,7 @@ from .codecs import CodecsModel from .drivers import DriversModel from .filesystem import FilesystemModel from .identity import IdentityModel +from .integrity import IntegrityModel from .kernel import KernelModel from .keyboard import KeyboardModel from .locale import LocaleModel @@ -179,6 +180,7 @@ class SubiquityModel: self.drivers = DriversModel() self.filesystem = FilesystemModel() self.identity = IdentityModel() + self.integrity = IntegrityModel() self.kernel = KernelModel() self.keyboard = KeyboardModel(self.root) self.locale = LocaleModel(self.chroot_prefix) diff --git a/subiquity/server/controllers/__init__.py b/subiquity/server/controllers/__init__.py index a609a4f1..bbe8191b 100644 --- a/subiquity/server/controllers/__init__.py +++ b/subiquity/server/controllers/__init__.py @@ -20,6 +20,7 @@ from .drivers import DriversController from .filesystem import FilesystemController from .identity import IdentityController from .install import InstallController +from .integrity import IntegrityController from .keyboard import KeyboardController from .kernel import KernelController from .locale import LocaleController @@ -47,6 +48,7 @@ __all__ = [ 'ErrorController', 'FilesystemController', 'IdentityController', + 'IntegrityController', 'InstallController', 'KernelController', 'KeyboardController', diff --git a/subiquity/server/controllers/integrity.py b/subiquity/server/controllers/integrity.py new file mode 100644 index 00000000..76fa71af --- /dev/null +++ b/subiquity/server/controllers/integrity.py @@ -0,0 +1,83 @@ +# Copyright 2022 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 . + +import json +import logging + +from subiquitycore.async_helpers import schedule_task +from subiquitycore.utils import astart_command +from subiquity.common.apidef import API +from subiquity.common.types import CasperMd5Results +from subiquity.server.controller import SubiquityController + + +log = logging.getLogger('subiquity.server.controllers.integrity') + +mock_pass = {'checksum_missmatch': [], 'result': 'pass'} +mock_skip = {'checksum_missmatch': [], 'result': 'skip'} +mock_fail = {'checksum_missmatch': ['./casper/initrd'], 'result': 'fail'} + + +class IntegrityController(SubiquityController): + + endpoint = API.integrity + + model_name = 'integrity' + result_filepath = '/run/casper-md5check.json' + + @property + def result(self): + return CasperMd5Results( + self.model.md5check_results.get('result', 'unknown')) + + async def GET(self) -> CasperMd5Results: + return self.result + + async def wait_casper_md5check(self): + if self.app.opts.dry_run: + return + proc = await astart_command([ + 'journalctl', + '--follow', + '--output', 'json', + '_PID=1', + 'UNIT=casper-md5check.service', + ]) + while True: + jsonbytes = await proc.stdout.readline() + data = json.loads(jsonbytes.decode('utf-8')) + if data.get('JOB_RESULT') == 'done': + break + proc.terminate() + + async def get_md5check_results(self): + if self.app.opts.dry_run: + return mock_fail + with open(self.result_filepath) as fp: + try: + ret = json.load(fp) + except json.JSONDecodeError as jde: + log.debug(f'error reading casper-md5check results: {jde}') + return {} + else: + log.debug(f'casper-md5check results: {ret}') + return ret + + async def md5check(self): + await self.wait_casper_md5check() + self.model.md5check_results = await self.get_md5check_results() + + def start(self): + self._md5check_task = schedule_task(self.md5check()) diff --git a/subiquity/server/controllers/tests/test_integrity.py b/subiquity/server/controllers/tests/test_integrity.py new file mode 100644 index 00000000..cb81ac30 --- /dev/null +++ b/subiquity/server/controllers/tests/test_integrity.py @@ -0,0 +1,49 @@ +# Copyright 2022 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 . + +from subiquitycore.tests import SubiTestCase + +from subiquity.common.types import CasperMd5Results +from subiquity.models.integrity import IntegrityModel +from subiquity.server.controllers.integrity import ( + IntegrityController, + mock_fail, + mock_pass, + mock_skip, +) +from subiquitycore.tests.mocks import make_app + + +class TestMd5Check(SubiTestCase): + def setUp(self): + self.app = make_app() + self.app.opts.bootloader = 'UEFI' + self.ic = IntegrityController(app=self.app) + self.ic.model = IntegrityModel() + + def test_pass(self): + self.ic.model.md5check_results = mock_pass + self.assertEqual(CasperMd5Results.PASS, self.ic.result) + + def test_skip(self): + self.ic.model.md5check_results = mock_skip + self.assertEqual(CasperMd5Results.SKIP, self.ic.result) + + def test_unknown(self): + self.assertEqual(CasperMd5Results.UNKNOWN, self.ic.result) + + def test_fail(self): + self.ic.model.md5check_results = mock_fail + self.assertEqual(CasperMd5Results.FAIL, self.ic.result) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index 7841bbec..fa78e386 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -244,6 +244,7 @@ class SubiquityServer(Application): "Locale", "Refresh", "Kernel", + "Integrity", "Keyboard", "Zdev", "Source", diff --git a/subiquity/ui/views/error.py b/subiquity/ui/views/error.py index b9b27850..8deecc17 100644 --- a/subiquity/ui/views/error.py +++ b/subiquity/ui/views/error.py @@ -51,6 +51,8 @@ from subiquity.common.errorreport import ( ErrorReportState, ) +from subiquity.common.types import CasperMd5Results + log = logging.getLogger('subiquity.ui.views.error') @@ -135,12 +137,19 @@ submit_text = _(""" If you want to help improve the installer, you can send an error report. """) +integrity_check_fail_text = _(""" +The install media checksum verification failed. It's possible that this crash +is related to that checksum failure. Consider verifying the install media and +retrying the install. +""") + class ErrorReportStretchy(Stretchy): def __init__(self, app, ref, interrupting=True): self.app = app self.error_ref = ref + self.integrity_check_result = None self.report = app.error_reporter.get(ref) self.pending = None if self.report is None: @@ -248,6 +257,10 @@ class ErrorReportStretchy(Stretchy): Text(""), self.spinner]) + if self.integrity_check_result == CasperMd5Results.FAIL: + widgets.append(Text("")) + widgets.append(Text(rewrap(_(integrity_check_fail_text)))) + if self.report and self.report.uploader: widgets.extend([Text(""), btns['cancel']]) elif self.interrupting: @@ -279,6 +292,7 @@ class ErrorReportStretchy(Stretchy): self.min_wait = asyncio.create_task(asyncio.sleep(1)) if self.report: self.error_ref = self.report.ref() + self.integrity_check_result = await self.app.client.integrity.GET() self.pile.contents[:] = [ (w, self.pile.options('pack')) for w in self._pile_elements()] if self.pile.selectable():