diff --git a/apt-deps.txt b/apt-deps.txt index 733336d5..1fe90443 100644 --- a/apt-deps.txt +++ b/apt-deps.txt @@ -1,6 +1,7 @@ build-essential cloud-init curl +dctrl-tools fuseiso gettext gir1.2-umockdev-1.0 diff --git a/snapcraft.yaml b/snapcraft.yaml index 814040c6..3aa0e27d 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -110,6 +110,7 @@ parts: # This list includes the dependencies for curtin and probert as well, # there doesn't seem to be any real benefit to listing them separately. - cloud-init + - dctrl-tools - iso-codes - libpython3-stdlib - libpython3.10-minimal diff --git a/subiquity/models/kernel.py b/subiquity/models/kernel.py index 121eae6a..6f17489c 100644 --- a/subiquity/models/kernel.py +++ b/subiquity/models/kernel.py @@ -29,13 +29,23 @@ class KernelModel: # should be True. explicitly_requested: bool = False - def render(self): + # If set to True, we won't request curthooks to install the kernel. + # We can use this option if the kernel is already part of the source image + # of if a kernel got installed using ubuntu-drivers. + curthooks_no_install: bool = False + + @property + def needed_kernel(self) -> Optional[str]: if self.metapkg_name_override is not None: - metapkg = self.metapkg_name_override - else: - metapkg = self.metapkg_name + return self.metapkg_name_override + return self.metapkg_name + + def render(self): + if self.curthooks_no_install: + return {'kernel': None} + return { 'kernel': { - 'package': metapkg, + 'package': self.needed_kernel, }, } diff --git a/subiquity/server/controllers/install.py b/subiquity/server/controllers/install.py index fa967558..d86a1123 100644 --- a/subiquity/server/controllers/install.py +++ b/subiquity/server/controllers/install.py @@ -34,7 +34,7 @@ from subiquitycore.async_helpers import ( ) from subiquitycore.context import with_context from subiquitycore.file_util import write_file, generate_config_yaml -from subiquitycore.utils import log_process_streams +from subiquitycore.utils import arun_command, log_process_streams from subiquity.common.errorreport import ErrorReportKind from subiquity.common.types import ( @@ -51,6 +51,9 @@ from subiquity.server.curtin import ( run_curtin_command, start_curtin_command, ) +from subiquity.server.kernel import ( + list_installed_kernels, + ) from subiquity.server.mounter import ( Mounter, ) @@ -312,6 +315,17 @@ class InstallController(SubiquityController): step_config=self.generic_config(), source=source, ) + if self.app.opts.dry_run: + # In dry-run, extract does not do anything. Let's create what's + # needed manually. Ideally, we would not hardcode + # var/lib/dpkg/status because it is an implementation detail. + status = "var/lib/dpkg/status" + (root / status).parent.mkdir(parents=True, exist_ok=True) + await arun_command([ + "cp", "-aT", "--", + str(Path("/") / status), + str(root / status), + ]) await self.setup_target(context=context) # For OEM, we basically mimic what ubuntu-drivers does: @@ -343,6 +357,12 @@ class InstallController(SubiquityController): for pkg in self.model.oem.metapkgs: await self.install_package(package=pkg.name) + # If we already have a kernel installed, don't bother requesting + # curthooks to install it again or we might end up with two + # kernels. + if await list_installed_kernels(Path(self.tpath())): + self.model.kernel.curthooks_no_install = True + await run_curtin_step( name="curthooks", stages=["curthooks"], step_config=self.generic_config(), diff --git a/subiquity/server/kernel.py b/subiquity/server/kernel.py index b66f1851..68e136de 100644 --- a/subiquity/server/kernel.py +++ b/subiquity/server/kernel.py @@ -13,7 +13,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import pathlib +import subprocess +from typing import List + from subiquitycore.lsb_release import lsb_release +from subiquitycore.utils import arun_command def flavor_to_pkgname(flavor: str, *, dry_run: bool) -> str: @@ -27,3 +32,28 @@ def flavor_to_pkgname(flavor: str, *, dry_run: bool) -> str: # that's a bit tricky until we get cleverer about # the apt config in general. return f'linux-{flavor}-{release}' + + +async def list_installed_kernels(rootfs: pathlib.Path) -> List[str]: + ''' Return the list of linux-image packages installed in rootfs. ''' + # TODO use python-apt instead coupled with rootdir. + # Ideally, we should not hardcode var/lib/dpkg/status which is an + # implementation detail. + try: + cp = await arun_command([ + 'grep-status', + '--whole-pkg', + '-FProvides', 'linux-image', '--and', '-FStatus', 'installed', + '--show-field=Package', + '--no-field-names', + str(rootfs / pathlib.Path('var/lib/dpkg/status')), + ], check=True) + except subprocess.CalledProcessError as cpe: + # grep-status exits with status 1 when there is no match. + if cpe.returncode != 1: + raise + stdout = cpe.stdout + else: + stdout = cp.stdout + + return [line for line in stdout.splitlines() if line] diff --git a/subiquity/server/tests/test_kernel.py b/subiquity/server/tests/test_kernel.py index 2a343b17..40a12c3a 100644 --- a/subiquity/server/tests/test_kernel.py +++ b/subiquity/server/tests/test_kernel.py @@ -13,9 +13,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import subprocess import unittest +from unittest import mock -from subiquity.server.kernel import flavor_to_pkgname +from subiquity.server.kernel import ( + flavor_to_pkgname, + list_installed_kernels, +) class TestFlavorToPkgname(unittest.TestCase): @@ -33,3 +38,36 @@ class TestFlavorToPkgname(unittest.TestCase): self.assertEqual('linux-generic-hwe-20.04', flavor_to_pkgname('generic-hwe', dry_run=True)) + + +class TestListInstalledKernels(unittest.IsolatedAsyncioTestCase): + async def test_one_kernel(self): + rv = subprocess.CompletedProcess([], 0) + rv.stdout = 'linux-image-6.1.0-16-generic' + with mock.patch('subiquity.server.kernel.arun_command', + return_value=rv) as mock_arun: + ret = await list_installed_kernels('/') + self.assertEqual(['linux-image-6.1.0-16-generic'], ret) + mock_arun.assert_called_once() + + async def test_two_kernels(self): + rv = subprocess.CompletedProcess([], 0) + rv.stdout = '''\ +linux-image-6.1.0-16-generic +linux-image-6.2.0-24-generic +''' + with mock.patch('subiquity.server.kernel.arun_command', + return_value=rv) as mock_arun: + ret = await list_installed_kernels('/') + self.assertEqual(['linux-image-6.1.0-16-generic', + 'linux-image-6.2.0-24-generic'], ret) + mock_arun.assert_called_once() + + async def test_no_kernel(self): + rv = subprocess.CompletedProcess([], 0) + rv.stdout = '\n' + with mock.patch('subiquity.server.kernel.arun_command', + return_value=rv) as mock_arun: + ret = await list_installed_kernels('/') + self.assertEqual([], ret) + mock_arun.assert_called_once()