Skip to content

Commit

Permalink
oem: ensure a single kernel gets installed
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
ogayot committed Jun 30, 2023
1 parent d7cd8e4 commit 537e6a8
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 7 deletions.
1 change: 1 addition & 0 deletions apt-deps.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
build-essential
cloud-init
curl
dctrl-tools
fuseiso
gettext
gir1.2-umockdev-1.0
Expand Down
1 change: 1 addition & 0 deletions snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions subiquity/models/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
22 changes: 21 additions & 1 deletion subiquity/server/controllers/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
)
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 (
Expand All @@ -51,6 +51,9 @@
run_curtin_command,
start_curtin_command,
)
from subiquity.server.kernel import (
list_installed_kernels,
)
from subiquity.server.mounter import (
Mounter,
)
Expand Down Expand Up @@ -305,6 +308,17 @@ async def run_curtin_step(name, stages, step_config, source=None):
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 = Path("var/lib/dpkg/status")
(Path(root) / status).parent.mkdir(parents=True, exist_ok=True)
await arun_command([
"cp", "-aT", "--",
str(Path("/") / status),
str(Path(root) / status),
])
await self.setup_target(context=context)

# For OEM, we basically mimic what ubuntu-drivers does:
Expand Down Expand Up @@ -340,6 +354,12 @@ async def run_curtin_step(name, stages, step_config, source=None):
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(),
Expand Down
32 changes: 32 additions & 0 deletions subiquity/server/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -27,3 +32,30 @@ 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,
*, grep_status: str = "grep-status") -> 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]
59 changes: 58 additions & 1 deletion subiquity/server/tests/test_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -33,3 +38,55 @@ def test_flavor_hwe(self):

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

async def test_alternate_paths(self):
rv = subprocess.CompletedProcess([], 0)
rv.stdout = ''
with mock.patch('subiquity.server.kernel.arun_command',
return_value=rv) as mock_arun:
await list_installed_kernels('/')

mock_arun.assert_called_once()
self.assertEqual(mock_arun.call_args.args[0][0], 'grep-status')

with mock.patch('subiquity.server.kernel.arun_command',
return_value=rv) as mock_arun:
await list_installed_kernels(
'/', grep_status='/usr/bin/grep-status')

mock_arun.assert_called_once()
self.assertEqual(mock_arun.call_args.args[0][0],
'/usr/bin/grep-status')

0 comments on commit 537e6a8

Please sign in to comment.