curtin-dev team mailing list archive
-
curtin-dev team
-
Mailing list archive
-
Message #03522
[Merge] ~dbungert/curtin:kernel-lowest-layer into curtin:master
Dan Bungert has proposed merging ~dbungert/curtin:kernel-lowest-layer into curtin:master.
Commit message:
do not squash
add distro capabilities for package removal, kernels especially, and have curthooks use that
Requested reviews:
curtin developers (curtin-dev)
For more details, see:
https://code.launchpad.net/~dbungert/curtin/+git/curtin/+merge/466708
--
Your team curtin developers is requested to review the proposed merge of ~dbungert/curtin:kernel-lowest-layer into curtin:master.
diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
index b278703..5a7a126 100644
--- a/curtin/commands/curthooks.py
+++ b/curtin/commands/curthooks.py
@@ -375,55 +375,69 @@ def install_kernel(cfg, target):
kernel_cfg = config.fromdict(config.KernelConfig, kernel_cfg_d)
- mapping = copy.deepcopy(KERNEL_MAPPING)
- config.merge_config(mapping, kernel_cfg.mapping)
-
- # Machines using flash-kernel may need additional dependencies installed
- # before running. Run those checks in the ephemeral environment so the
- # target only has required packages installed. See LP: #1640519
- fk_packages = get_flash_kernel_pkgs()
- if fk_packages:
- distro.install_packages(fk_packages.split(), target=target)
-
- if kernel_cfg.package:
- install(kernel_cfg.package)
+ if not kernel_cfg.install:
+ if kernel_cfg.remove_existing:
+ distro.purge_all_kernels(target=target)
+ LOG.debug(
+ "Not installing any kernel since kernel: {install: false} "
+ "was specified"
+ )
return
- # uname[2] is kernel name (ie: 3.16.0-7-generic)
- # version gets X.Y.Z, flavor gets anything after second '-'.
- kernel = os.uname()[2]
- codename, _ = util.subp(['lsb_release', '--codename', '--short'],
- capture=True, target=target)
- codename = codename.strip()
- version, abi, flavor = kernel.split('-', 2)
+ with contextlib.ExitStack() as exitstack:
+ if kernel_cfg.remove_existing:
+ exitstack.enter_context(distro.purge_kernels(target=target))
- try:
- map_suffix = mapping[codename][version]
- except KeyError:
- LOG.warn("Couldn't detect kernel package to install for %s."
- % kernel)
- if kernel_cfg.fallback_package is not None:
- install(kernel_cfg.fallback_package)
- return
+ mapping = copy.deepcopy(KERNEL_MAPPING)
+ config.merge_config(mapping, kernel_cfg.mapping)
- package = "linux-{flavor}{map_suffix}".format(
- flavor=flavor, map_suffix=map_suffix)
+ # Machines using flash-kernel may need additional dependencies
+ # installed before running. Run those checks in the ephemeral
+ # environment so the target only has required packages installed.
+ # See LP: #1640519
+ fk_packages = get_flash_kernel_pkgs()
+ if fk_packages:
+ distro.install_packages(fk_packages.split(), target=target)
- if distro.has_pkg_available(package, target):
- if distro.has_pkg_installed(package, target):
- LOG.debug("Kernel package '%s' already installed", package)
- else:
- LOG.debug("installing kernel package '%s'", package)
- install(package)
- else:
- if kernel_cfg.fallback_package is not None:
- LOG.info("Kernel package '%s' not available. "
- "Installing fallback package '%s'.",
- package, kernel_cfg.fallback_package)
- install(kernel_cfg.fallback_package)
+ if kernel_cfg.package:
+ install(kernel_cfg.package)
+ return
+
+ # uname[2] is kernel name (ie: 3.16.0-7-generic)
+ # version gets X.Y.Z, flavor gets anything after second '-'.
+ kernel = os.uname()[2]
+ codename, _ = util.subp(['lsb_release', '--codename', '--short'],
+ capture=True, target=target)
+ codename = codename.strip()
+ version, abi, flavor = kernel.split('-', 2)
+
+ try:
+ map_suffix = mapping[codename][version]
+ except KeyError:
+ LOG.warn("Couldn't detect kernel package to install for %s."
+ % kernel)
+ if kernel_cfg.fallback_package is not None:
+ install(kernel_cfg.fallback_package)
+ return
+
+ package = "linux-{flavor}{map_suffix}".format(
+ flavor=flavor, map_suffix=map_suffix)
+
+ if distro.has_pkg_available(package, target):
+ if distro.has_pkg_installed(package, target):
+ LOG.debug("Kernel package '%s' already installed", package)
+ else:
+ LOG.debug("installing kernel package '%s'", package)
+ install(package)
else:
- LOG.warn("Kernel package '%s' not available and no fallback."
- " System may not boot.", package)
+ if kernel_cfg.fallback_package is not None:
+ LOG.info("Kernel package '%s' not available. "
+ "Installing fallback package '%s'.",
+ package, kernel_cfg.fallback_package)
+ install(kernel_cfg.fallback_package)
+ else:
+ LOG.warn("Kernel package '%s' not available and no fallback."
+ " System may not boot.", package)
def uefi_remove_old_loaders(grubcfg: config.GrubConfig, target: str):
diff --git a/curtin/config.py b/curtin/config.py
index 2dff329..e9f578d 100644
--- a/curtin/config.py
+++ b/curtin/config.py
@@ -159,6 +159,8 @@ class KernelConfig:
package: typing.Optional[str] = None
fallback_package: str = "linux-generic"
mapping: dict = attr.Factory(dict)
+ install: bool = attr.ib(default=True, converter=value_as_boolean)
+ remove_existing: bool = attr.ib(default=False, converter=value_as_boolean)
class SerializationError(Exception):
diff --git a/curtin/distro.py b/curtin/distro.py
index 4664320..973eb35 100644
--- a/curtin/distro.py
+++ b/curtin/distro.py
@@ -1,5 +1,6 @@
# This file is part of curtin. See LICENSE file for copyright and license info.
from collections import namedtuple
+from contextlib import contextmanager
import os
import re
import textwrap
@@ -477,6 +478,105 @@ def install_packages(pkglist, osfamily=None, opts=None, target=None, env=None,
assume_downloaded=assume_downloaded)
+def grep_status_list_kernels(target=None):
+ target = target_path(target)
+ cmd = [
+ "grep-status",
+ "--whole-pkg",
+ "-FProvides",
+ "linux-image",
+ "--and",
+ "-FStatus",
+ "installed",
+ "--show-field=Package",
+ "--no-field-names",
+ ]
+ out, _ = subp(cmd, capture=True, target=target, rcs=(0, 1))
+ return out.splitlines()
+
+
+def list_kernels(osfamily=None, target=None):
+ if not osfamily:
+ osfamily = get_osfamily(target=target)
+
+ distro_cfg = {
+ DISTROS.debian: grep_status_list_kernels
+ }
+
+ list_kernels_cmd = distro_cfg.get(osfamily)
+ if list_kernels_cmd is None:
+ raise ValueError('No package purge command for distro: %s' %
+ osfamily)
+
+ return list_kernels_cmd(target=target)
+
+
+@contextmanager
+def purge_kernels(osfamily=None, target=None):
+ before = set(list_kernels(osfamily=osfamily, target=target))
+ yield
+
+ if not bool(before):
+ LOG.debug('No kernels to remove - no kernels preinstalled')
+ return
+
+ # the second part of purge_kernels is intended to be run after any kernels
+ # we're installing have been installed. We expect the list of kernels to
+ # grow as a result. So if the set has not changed, that means that the
+ # kernel we asked to install was installed already, so we shouldn't be
+ # removing one. This should work fine for a single kernel being
+ # preinstalled, but will fail to remove in the case of 2 kernels
+ # preinstalled but only one of them is intended.
+ after = set(list_kernels(osfamily=osfamily, target=target))
+ if not bool(after - before):
+ LOG.debug(
+ 'No kernels to remove - kernel to install was already installed'
+ )
+ return
+
+ purge_packages(list(before), target=target)
+
+
+def purge_all_kernels(osfamily=None, target=None):
+ kernels = list_kernels(osfamily=osfamily, target=target)
+ if not bool(kernels):
+ LOG.debug('No kernels to remove - no kernels preinstalled')
+ return
+
+ purge_packages(kernels, target=target)
+
+
+def purge_packages(pkglist, osfamily=None, opts=None, target=None, env=None):
+ if isinstance(pkglist, str):
+ pkglist = [pkglist]
+
+ LOG.debug('Removing packages %s', pkglist)
+
+ if not osfamily:
+ osfamily = get_osfamily(target=target)
+
+ distro_cfg = {
+ DISTROS.debian: {'function': run_apt_command,
+ 'subcommands': ('purge', 'autoremove')},
+ }
+
+ purge_cmd = distro_cfg.get(osfamily)
+ if not purge_cmd:
+ raise ValueError('No package purge command for distro: %s' %
+ osfamily)
+
+ for mode in distro_cfg[osfamily]['subcommands']:
+ ret = distro_cfg[osfamily]['function'](
+ mode,
+ args=pkglist,
+ opts=opts,
+ target=target,
+ env=env,
+ assume_downloaded=True,
+ )
+ return ret
+
+
def has_pkg_available(pkg, target=None, osfamily=None):
if not osfamily:
osfamily = get_osfamily(target=target)
diff --git a/doc/topics/config.rst b/doc/topics/config.rst
index 5671200..d511f94 100644
--- a/doc/topics/config.rst
+++ b/doc/topics/config.rst
@@ -434,14 +434,37 @@ Default mapping for Releases to package names is as follows::
Specify the exact package to install in the target OS.
-**Example**::
+**install**: *<boolean>*
+
+Defaults to True. If False, no kernel install is attempted.
+
+**remove_existing**: *<boolean>*
+
+Supported on Debian and Ubuntu OSes. Defaults to False. If True, known
+kernels in .deb packages are removed from the target system (packages which
+``Provides: linux-image``), followed by an ``apt-get autoremove``. If no
+kernel is being installed, this also implies the removal of the
+``linux-firmware`` package.
+
+**Examples**::
kernel:
fallback-package: linux-image-generic
package: linux-image-generic-lts-xenial
mapping:
- xenial:
- - 4.4.0: -my-custom-kernel
+ - 4.4.0: -my-custom-kernel
+
+ # install this kernel if not yet installed,
+ # and remove other kernels if present
+ kernel:
+ package: linux-image-generic-hwe-24.04
+ remove_existing: true
+
+ # install nothing and remove existing kernels
+ kernel:
+ install: false
+ remove_existing: true
kexec
diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
index b5666e2..6819f7a 100644
--- a/tests/unittests/test_curthooks.py
+++ b/tests/unittests/test_curthooks.py
@@ -6,6 +6,7 @@ import textwrap
from typing import Optional
import attr
+from parameterized import parameterized
from curtin.commands import curthooks
from curtin.commands.block_meta import extract_storage_ordered_dict
@@ -58,12 +59,20 @@ class TestCurthooksInstallKernel(CiTestCase):
ccc = 'curtin.commands.curthooks'
self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg')
self.add_patch('curtin.distro.install_packages', 'mock_instpkg')
+ self.add_patch('curtin.distro.purge_packages', 'mock_purgepkg')
+ self.add_patch(
+ 'curtin.distro.grep_status_list_kernels', 'mock_list_kernels',
+ )
+ self.add_patch(
+ 'curtin.distro.os_release', return_value={"ID": "ubuntu"}
+ )
self.add_patch(ccc + '.os.uname', 'mock_uname')
self.add_patch(ccc + '.util.subp', 'mock_subp')
self.add_patch(
ccc + '.get_flash_kernel_pkgs',
'mock_get_flash_kernel_pkgs')
+ self.mock_get_flash_kernel_pkgs.return_value = None
self.fk_env = {'FK_FORCE': 'yes', 'FK_FORCE_CONTAINER': 'yes'}
# Tests don't actually install anything so we just need a name
self.target = self.tmp_dir()
@@ -85,7 +94,6 @@ class TestCurthooksInstallKernel(CiTestCase):
def test__installs_kernel_package(self):
kernel_package = "mock-linux-kernel"
kernel_cfg = {'kernel': {'package': kernel_package}}
- self.mock_get_flash_kernel_pkgs.return_value = None
with patch.dict(os.environ, clear=True):
curthooks.install_kernel(kernel_cfg, self.target)
@@ -125,13 +133,93 @@ class TestCurthooksInstallKernel(CiTestCase):
["linux-flavor-lts-dapper"],
target=self.target, env=self.fk_env)
- def test__installs_kernel_null(self):
- kernel_cfg = {'kernel': None}
+ @parameterized.expand((
+ [{'kernel': None}],
+ [{'kernel': {'install': 'false'}}],
+ ))
+ def test__not_installs_kernel(self, kernel_cfg):
with patch.dict(os.environ, clear=True):
curthooks.install_kernel(kernel_cfg, self.target)
self.mock_instpkg.assert_not_called()
+ def test__removes_and_installs_kernel(self):
+ to_install_kernel_package = "mock-linux-kernel"
+ to_remove_kernel_package = "mock-to-remove"
+ kernel_cfg = {
+ 'kernel': {
+ 'package': to_install_kernel_package,
+ 'remove_existing': 'true',
+ }
+ }
+ self.mock_subp.return_value = ("warty", "")
+ self.mock_uname.return_value = (None, None, "1.2.3-4-flavor")
+ self.mock_list_kernels.side_effect = [
+ [to_remove_kernel_package],
+ [to_install_kernel_package, to_remove_kernel_package],
+ ]
+
+ with patch.dict(os.environ, clear=True):
+ curthooks.install_kernel(kernel_cfg, self.target)
+
+ self.mock_instpkg.assert_called_with(
+ [to_install_kernel_package],
+ target=self.target,
+ env=self.fk_env,
+ )
+ self.mock_purgepkg.assert_called_with(
+ [to_remove_kernel_package], target=self.target
+ )
+
+ def test__installs_kernel_nothing_to_remove(self):
+ to_install_kernel_package = "mock-linux-kernel"
+ kernel_cfg = {
+ 'kernel': {
+ 'package': to_install_kernel_package,
+ 'remove_existing': 'true',
+ }
+ }
+ self.mock_subp.return_value = ("warty", "")
+ self.mock_uname.return_value = (None, None, "1.2.3-4-flavor")
+ self.mock_list_kernels.return_value = []
+
+ with patch.dict(os.environ, clear=True):
+ curthooks.install_kernel(kernel_cfg, self.target)
+
+ self.mock_instpkg.assert_called_with(
+ [to_install_kernel_package],
+ target=self.target,
+ env=self.fk_env,
+ )
+ self.mock_purgepkg.assert_not_called()
+
+ def test__target_already_has_kernel(self):
+ to_install_kernel_package = "mock-linux-kernel"
+ kernel_cfg = {
+ 'kernel': {
+ 'package': to_install_kernel_package,
+ 'remove_existing': 'true',
+ }
+ }
+ self.mock_subp.return_value = ("warty", "")
+ self.mock_uname.return_value = (None, None, "1.2.3-4-flavor")
+ self.mock_list_kernels.return_value = ["mock-kernel-1.2.3-4-generic"]
+
+ with patch.dict(os.environ, clear=True):
+ curthooks.install_kernel(kernel_cfg, self.target)
+
+ # the mapping from kernel to install and what list_kernls returns
+ # is not straightforward, so we ask apt to install the package and
+ # apt shouldn't have to do very much
+ self.mock_instpkg.assert_called_with(
+ [to_install_kernel_package],
+ target=self.target,
+ env=self.fk_env,
+ )
+ # but because nothing actually gets installed, there is nothing to
+ # remove
+ self.mock_purgepkg.assert_not_called()
+
class TestEnableDisableUpdateInitramfs(CiTestCase):
diff --git a/tools/vmtest-system-setup b/tools/vmtest-system-setup
index 2f82464..482e431 100755
--- a/tools/vmtest-system-setup
+++ b/tools/vmtest-system-setup
@@ -15,6 +15,7 @@ DEPS=(
build-essential
cloud-image-utils
cryptsetup
+ dctrl-tools
git
make
net-tools
Follow ups