oem: ensure a single kernel gets installed

Before running curthooks, we now look in the target if there is an
installed kernel. If there is, we instruct curtin _not_ to install
another one.

Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
This commit is contained in:
Olivier Gayot 2023-06-28 15:27:13 +02:00
parent b844119b79
commit 6424c3c52b
6 changed files with 107 additions and 7 deletions

View File

@ -1,6 +1,7 @@
build-essential
cloud-init
curl
dctrl-tools
fuseiso
gettext
gir1.2-umockdev-1.0

View File

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

View File

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

View File

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

View File

@ -13,7 +13,12 @@
# 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 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]

View File

@ -13,9 +13,14 @@
# 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 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()