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:
parent
f71de2e16d
commit
cd8667d6a1
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 = {}
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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())
|
|
@ -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)
|
|
@ -244,6 +244,7 @@ class SubiquityServer(Application):
|
|||
"Locale",
|
||||
"Refresh",
|
||||
"Kernel",
|
||||
"Integrity",
|
||||
"Keyboard",
|
||||
"Zdev",
|
||||
"Source",
|
||||
|
|
|
@ -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():
|
||||
|
|
Loading…
Reference in New Issue