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
This commit is contained in:
Dan Bungert 2022-11-07 17:51:53 -07:00
parent f71de2e16d
commit cd8667d6a1
9 changed files with 180 additions and 0 deletions

View File

@ -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()

View File

@ -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'

View File

@ -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 <http://www.gnu.org/licenses/>.
class IntegrityModel:
md5check_results = {}

View File

@ -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)

View File

@ -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',

View File

@ -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 <http://www.gnu.org/licenses/>.
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())

View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -244,6 +244,7 @@ class SubiquityServer(Application):
"Locale",
"Refresh",
"Kernel",
"Integrity",
"Keyboard",
"Zdev",
"Source",

View File

@ -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():