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