← Back to team overview

curtin-dev team mailing list archive

[Merge] ~dbungert/curtin:kernel-remove-tweaks into curtin:master

 

Dan Bungert has proposed merging ~dbungert/curtin:kernel-remove-tweaks into curtin:master.

Commit message:
Do not squash

Update config kernel interface to merge the two removal options into one keyword.
Modest test coverage improvements.
Comment improvements.
Minor logic clarification.

Requested reviews:
  curtin developers (curtin-dev)

For more details, see:
https://code.launchpad.net/~dbungert/curtin/+git/curtin/+merge/473473
-- 
Your team curtin developers is requested to review the proposed merge of ~dbungert/curtin:kernel-remove-tweaks into curtin:master.
diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
index 25568b0..a8cc4c6 100644
--- a/curtin/commands/curthooks.py
+++ b/curtin/commands/curthooks.py
@@ -374,10 +374,10 @@ def install_kernel(cfg, target):
     kernel_cfg = config.fromdict(config.KernelConfig, kernel_cfg_d)
 
     with contextlib.ExitStack() as exitstack:
-        if kernel_cfg.remove_existing or kernel_cfg.remove:
+        if kernel_cfg.remove_needed():
             exitstack.enter_context(
                 distro.ensure_one_kernel(
-                    target=target, before=kernel_cfg.remove,
+                    target=target, before=kernel_cfg.kernels_to_remove(),
                 )
             )
         if not kernel_cfg.install:
diff --git a/curtin/config.py b/curtin/config.py
index 74451c8..037b4bb 100644
--- a/curtin/config.py
+++ b/curtin/config.py
@@ -160,8 +160,17 @@ class KernelConfig:
     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)
-    remove: list = attr.Factory(list)
+    remove: typing.Union[None | list | str] = None
+
+    def remove_needed(self) -> bool:
+        if to_remove := self.kernels_to_remove():
+            return bool(to_remove)
+        return self.remove == "existing"
+
+    def kernels_to_remove(self) -> typing.Optional[list]:
+        if isinstance(self.remove, list):
+            return self.remove
+        return None
 
 
 class SerializationError(Exception):
@@ -225,14 +234,18 @@ class Deserializer:
             ]
 
     def _walk_Union(self, meth, args, context):
+        if context.cur is None:
+            return context.cur
         NoneType = type(None)
         if NoneType in args:
             args = [a for a in args if a is not NoneType]
             if len(args) == 1:
                 # I.e. Optional[thing]
-                if context.cur is None:
-                    return context.cur
                 return meth(args[0], context)
+        if isinstance(context.cur, list):
+            return meth(list, context)
+        if isinstance(context.cur, str):
+            return meth(str, context)
         context.error(f"cannot serialize Union[{args}]")
 
     def _deserialize_attr(self, annotation, context):
diff --git a/curtin/distro.py b/curtin/distro.py
index 219b854..e6e13d5 100644
--- a/curtin/distro.py
+++ b/curtin/distro.py
@@ -519,7 +519,7 @@ def list_kernels(osfamily=None, target=None):
 def ensure_one_kernel(osfamily=None, target=None, before=None):
     """ensure_one_kernel is a context manager that evalutates the state of
     installed kernels, before and after doing package operations.  With that
-    information, kernels that only appear before and not after are removed.
+    information, kernels that are unique to the before set are removed.
     """
     if bool(before):
         before = set(before)
@@ -538,10 +538,12 @@ def ensure_one_kernel(osfamily=None, target=None, before=None):
     # 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.
+    # preinstalled but only one of them is intended.  We seed the before list
+    # externally to handle that case, when the above `before = list_kernels()`
+    # is too late to accurately capture the initial state.
     after = set(list_kernels(osfamily=osfamily, target=target))
     LOG.debug('ensure_one_kernel: kernels after install %s', after)
-    if not bool(after - before):
+    if before == after:
         LOG.debug(
             'No kernels to remove - kernel to install was already installed'
         )
diff --git a/doc/topics/config.rst b/doc/topics/config.rst
index fb0916f..4fbcc5a 100644
--- a/doc/topics/config.rst
+++ b/doc/topics/config.rst
@@ -439,17 +439,21 @@ Specify the exact package to install in the target OS.
 
 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.
+**remove**: *<List of kernel package names> | "existing"*
 
-**remove**: *<List of kernel package names>*
+After kernel installation, remove the listed packages.  Defaults to no action.
 
-After kernel installation, remove the listed packages.
+Instead of a list, the keyword ``existing`` may be supplied, indicating that
+all existing kernels must be removed.  (Supported on Debian and Ubuntu OSes)
+Known kernels in .deb packages are removed from the target system (packages
+which ``Provides: linux-image``), followed by an ``apt-get autoremove``.
+It's expected that some sort of other kernel is specified with ``package`` to
+be the replacement.
+
+The kernel removal code has a guardrail against the case where the target
+system is left without a kernel installed, in which case this ``remove``
+directive may be ignored.
 
 **Examples**::
 
@@ -464,12 +468,7 @@ After kernel installation, remove the listed packages.
   # 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
+    remove: existing
 
   # install hwe kernel, remove generic kernel
   kernel:
diff --git a/tests/unittests/test_config.py b/tests/unittests/test_config.py
index 603a5c9..1db3191 100644
--- a/tests/unittests/test_config.py
+++ b/tests/unittests/test_config.py
@@ -206,5 +206,24 @@ class TestDeserializer(CiTestCase):
             DashToUnderscore(a_b=True),
             deserializer.deserialize(DashToUnderscore, {"a-b": True}))
 
+    def test_union_str_list(self):
+        deserializer = config.Deserializer()
+
+        @attr.s(auto_attribs=True)
+        class UnionClass:
+            val: typing.Union[str | list | None]
+
+        self.assertEqual(
+            UnionClass(val="a"),
+            deserializer.deserialize(UnionClass, {"val": "a"}))
+
+        self.assertEqual(
+            UnionClass(val=["b"]),
+            deserializer.deserialize(UnionClass, {"val": ["b"]}))
+
+        self.assertEqual(
+            UnionClass(val=None),
+            deserializer.deserialize(UnionClass, {"val": None}))
+
 
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
index 4ee3a26..f0b263e 100644
--- a/tests/unittests/test_curthooks.py
+++ b/tests/unittests/test_curthooks.py
@@ -98,6 +98,7 @@ class TestCurthooksInstallKernel(CiTestCase):
 
             self.mock_instpkg.assert_called_with(
                 [kernel_package], target=self.target, env=self.fk_env)
+            self.mock_purgepkg.assert_not_called()
 
     def test__installs_kernel_fallback_package(self):
         fallback_package = "mock-linux-kernel-fallback"
@@ -111,6 +112,7 @@ class TestCurthooksInstallKernel(CiTestCase):
 
             self.mock_instpkg.assert_called_with(
                 [fallback_package], target=self.target, env=self.fk_env)
+            self.mock_purgepkg.assert_not_called()
 
     def test__installs_kernel_from_mapping(self):
         kernel_cfg = {
@@ -131,6 +133,7 @@ class TestCurthooksInstallKernel(CiTestCase):
             self.mock_instpkg.assert_called_with(
                 ["linux-flavor-lts-dapper"],
                 target=self.target, env=self.fk_env)
+            self.mock_purgepkg.assert_not_called()
 
     @parameterized.expand((
         [{'kernel': None}],
@@ -141,6 +144,7 @@ class TestCurthooksInstallKernel(CiTestCase):
             curthooks.install_kernel(kernel_cfg, self.target)
 
             self.mock_instpkg.assert_not_called()
+            self.mock_purgepkg.assert_not_called()
 
     def test__removes_and_installs_kernel(self):
         to_install_kernel_package = "mock-linux-kernel"
@@ -148,7 +152,7 @@ class TestCurthooksInstallKernel(CiTestCase):
         kernel_cfg = {
             'kernel': {
                 'package': to_install_kernel_package,
-                'remove_existing': 'true',
+                'remove': 'existing',
             }
         }
         self.mock_subp.return_value = ("warty", "")
@@ -175,7 +179,7 @@ class TestCurthooksInstallKernel(CiTestCase):
         kernel_cfg = {
             'kernel': {
                 'package': to_install_kernel_package,
-                'remove_existing': 'true',
+                'remove': 'existing',
             }
         }
         self.mock_subp.return_value = ("warty", "")
@@ -197,7 +201,7 @@ class TestCurthooksInstallKernel(CiTestCase):
         kernel_cfg = {
             'kernel': {
                 'package': to_install_kernel_package,
-                'remove_existing': 'true',
+                'remove': 'existing',
             }
         }
         self.mock_subp.return_value = ("warty", "")

Follow ups