← Back to team overview

curtin-dev team mailing list archive

[Merge] ~sjg1/curtin:ext into curtin:master

 

Simon Glass has proposed merging ~sjg1/curtin:ext into curtin:master.

Commit message:
This is the -master version of changes to fully support extlinux in Curtin.

This is only unit-tested so far, but future work will add some vmtests.

Requested reviews:
  Dan Bungert (dbungert)

For more details, see:
https://code.launchpad.net/~sjg1/curtin/+git/curtin/+merge/484176
-- 
Your team curtin developers is subscribed to branch curtin:master.
diff --git a/HACKING.rst b/HACKING.rst
index f2b618d..f723906 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -46,6 +46,12 @@ Do these things once
     git remote add LP_USER ssh://LP_USER@xxxxxxxxxxxxxxxxx/~LP_USER/curtin
     git push LP_USER master
 
+* Install `libapt-pkg-dev` as it is needed by tox:
+
+  .. code:: sh
+
+    sudo apt install libapt-pkg-dev
+
 .. _repository: https://git.launchpad.net/curtin
 .. _contributor license agreement: https://ubuntu.com/legal/contributors
 .. _contributor-agreement-canonical: https://launchpad.net/%7Econtributor-agreement-canonical/+members
diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
index 5749647..7c053e4 100644
--- a/curtin/commands/block_meta.py
+++ b/curtin/commands/block_meta.py
@@ -1366,6 +1366,19 @@ def proc_filesystems_passno(fstype):
     return "1"
 
 
+def resolve_fdata_spec(fdata: FstabData) -> str:
+    """Get the spec to use for an fdata item (like a line in /etc/fstab)
+
+    :param: Data to look at
+    Return: The correct spec to use, e.g. /dev/sda1'
+    """
+    if fdata.spec is None:
+        if not fdata.device:
+            raise ValueError("FstabData missing both spec and device.")
+        return get_volume_spec(fdata.device)
+    return fdata.spec
+
+
 def fstab_line_for_data(fdata):
     """Return a string representing fdata in /etc/fstab format.
 
@@ -1378,12 +1391,7 @@ def fstab_line_for_data(fdata):
         else:
             raise ValueError("empty path in %s." % str(fdata))
 
-    if fdata.spec is None:
-        if not fdata.device:
-            raise ValueError("FstabData missing both spec and device.")
-        spec = get_volume_spec(fdata.device)
-    else:
-        spec = fdata.spec
+    spec = resolve_fdata_spec(fdata)
 
     if fdata.options in (None, "", "defaults"):
         if fdata.fstype == "swap":
diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
index 0c4d7fd..34c4a5d 100644
--- a/curtin/commands/curthooks.py
+++ b/curtin/commands/curthooks.py
@@ -30,7 +30,7 @@ from curtin.block import deps as bdeps
 from curtin.distro import DISTROS
 from curtin.net import deps as ndeps
 from curtin.reporter import events
-from curtin.commands import apply_net, apt_config
+from curtin.commands import apply_net, apt_config, block_meta
 from curtin.commands.install_grub import install_grub
 from curtin.commands.install_extlinux import install_extlinux
 from curtin.url_helper import get_maas_version
@@ -414,8 +414,8 @@ def install_kernel(cfg, target):
         try:
             map_suffix = mapping[codename][version]
         except KeyError:
-            LOG.warn("Couldn't detect kernel package to install for %s."
-                     % kernel)
+            LOG.warning("Couldn't detect kernel package to install for %s."
+                        % kernel)
             if kernel_cfg.fallback_package is not None:
                 install(kernel_cfg.fallback_package)
             return
@@ -873,6 +873,38 @@ def translate_old_grub_schema(cfg):
     cfg['boot'] = grub_cfg
 
 
+def setup_extlinux(
+        cfg: dict,
+        target: str):
+    """Set up an extlinux.conf file
+
+    :param: cfg: A config dict containing config.BootCfg in cfg['boot'].
+    # :param: target: A string specifying the path to the chroot mountpoint.
+    """
+    bootcfg = config.fromdict(config.BootCfg, cfg.get('boot', {}))
+
+    # If there is a separate boot partition, set fw_boot_dir to empty
+    storage_cfg = cfg.get('storage', {}).get('config', {})
+    fw_boot_dir = '/boot'
+    root = None
+    for item in storage_cfg:
+        if item['type'] == 'mount':
+            if item['path'] == '/boot':
+                fw_boot_dir = ''
+            elif item['path'] == '/':
+                root = item
+
+    if not root:
+        raise ValueError("Storage configuration has no root directory")
+
+    storage_config_dict = block_meta.extract_storage_ordered_dict(cfg)
+
+    fdata = block_meta.mount_data(root, storage_config_dict)
+    spec = block_meta.resolve_fdata_spec(fdata)
+
+    install_extlinux(bootcfg, target, fw_boot_dir, spec)
+
+
 def setup_boot(
         cfg: dict,
         target: str,
@@ -905,7 +937,7 @@ def setup_boot(
             if machine not in ['i586', 'i686', 'x86_64']:
                 raise ValueError('Invalid arch %s: Only x86 platforms support '
                                  'extlinux at present' % machine)
-            install_extlinux(cfg, target)
+            setup_extlinux(cfg, target)
 
     if machine == 's390x':
         with events.ReportEventStack(
@@ -1931,7 +1963,14 @@ def uses_grub(machine):
     return True
 
 
-def builtin_curthooks(cfg, target, state):
+def builtin_curthooks(cfg: dict, target: str, state: dict):
+    """Run the built-in curthooks
+
+    :param: cfg: Config dict
+    :param: target: Target directory for the new installation
+    :param: state: State information obtained from environment variables. See
+        load_command_environment()
+    """
     LOG.info('Running curtin builtin curthooks')
     stack_prefix = state.get('report_stack_prefix', '')
     state_etcd = os.path.split(state['fstab'])[0]
diff --git a/curtin/commands/install_extlinux.py b/curtin/commands/install_extlinux.py
index b52b591..dc1bd64 100644
--- a/curtin/commands/install_extlinux.py
+++ b/curtin/commands/install_extlinux.py
@@ -7,12 +7,14 @@ import os
 
 from curtin import config
 from curtin import paths
+from curtin import util
 from curtin.log import LOG
 
 EXTLINUX_DIR = '/boot/extlinux'
 
 
-def build_content(bootcfg: config.BootCfg, target: str):
+def build_content(bootcfg: config.BootCfg, target: str, fw_boot_dir: str,
+                  root_spec: str):
     """Build the content of the extlinux.conf file
 
     For now this only supports x86, since it does not handle the 'fdt' option.
@@ -21,18 +23,25 @@ def build_content(bootcfg: config.BootCfg, target: str):
     associated with fdt and fdtdir options.
 
     We assume that the boot directory is available as /boot in the target.
+
+    :param: bootcfg: A boot-config dict
+    :param: target: A string specifying the path to the chroot mountpoint.
+    :param: fw_boot_dir: Firmware's view of the /boot directory; when there is
+        a separate /boot partition, firmware will access that as the root
+        directory of the filesystem, so '' should be passed here. When the boot
+        directory is just a subdirectory of '/', then '/boot' should be passed
+    :param: root_spec: Root device to pass to kernel
     """
     def get_entry(label, params, menu_label_append=''):
         return f'''\
 label {label}
 \tmenu label {menu_label} {version}{menu_label_append}
-\tlinux /{kernel_path}
-\tinitrd /{initrd_path}
+\tlinux {fw_boot_dir}/{kernel_path}
+\tinitrd {fw_boot_dir}/{initrd_path}
 \tappend {params}'''
 
     buf = io.StringIO()
-    params = 'ro quiet'
-    alternatives = ['default', 'recovery']
+    params = f'{root_spec} ro quiet'
     menu_label = 'Linux'
 
     # For the recovery option, remove 'quiet' and add 'single'
@@ -56,10 +65,11 @@ timeout 50''', file=buf)
         LOG.debug('P: Writing config for %s...', kernel_path)
         initrd_path = os.path.basename(full_initrd_path)
         print(file=buf)
-        print(file=buf)
-        print(get_entry(f'l{seq}', params), file=buf)
+        if 'default' in bootcfg.alternatives:
+            print(file=buf)
+            print(get_entry(f'l{seq}', params), file=buf)
 
-        if 'recovery' in alternatives:
+        if 'rescue' in bootcfg.alternatives:
             print(file=buf)
             print(get_entry(f'l{seq}r', rec_params, ' (rescue target)'),
                   file=buf)
@@ -70,14 +80,20 @@ timeout 50''', file=buf)
 def install_extlinux(
         bootcfg: config.BootCfg,
         target: str,
+        fw_boot_dir: str,
+        root_spec: str,
         ):
     """Install extlinux to the target chroot.
 
-    :param: bootcfg: An config dict with grub config options.
+    :param: bootcfg: A boot-config dict.
     :param: target: A string specifying the path to the chroot mountpoint.
+    :param: fw_boot_dir: Firmware's view of the /boot directory
+    :param: root_spec: Root device to pass to kernel
     """
-    content = build_content(bootcfg, target)
+    LOG.debug("P: Writing extlinux, fw_boot_dir '%s' root_spec '%s'...",
+              fw_boot_dir, root_spec)
+    content = build_content(bootcfg, target, fw_boot_dir, root_spec)
     extlinux_path = paths.target_path(target, '/boot/extlinux')
-    os.makedirs(extlinux_path, exist_ok=True)
+    util.ensure_dir(extlinux_path)
     with open(extlinux_path + '/extlinux.conf', 'w', encoding='utf-8') as outf:
         outf.write(content)
diff --git a/curtin/config.py b/curtin/config.py
index da9e486..c5182a5 100644
--- a/curtin/config.py
+++ b/curtin/config.py
@@ -148,6 +148,18 @@ def _check_bootloaders(inst, attr, vals):
             raise ValueError(f'Unknown bootloader {val}: {vals}')
 
 
+def _check_alternatives(inst, attr, vals):
+    if not isinstance(vals, list):
+        raise ValueError(f'alternatives must be a list: {vals}')
+    if not vals:
+        raise ValueError(f'Empty alternatives list: {vals}')
+    if len(vals) != len(set(vals)):
+        raise ValueError(f'alternatives list contains duplicates: {vals}')
+    for val in vals:
+        if val not in ['default', 'rescue']:
+            raise ValueError(f'Unknown alternative {val}: {vals}')
+
+
 @attr.s(auto_attribs=True)
 class BootCfg:
     bootloaders: typing.List[str] = attr.ib(validator=_check_bootloaders)
@@ -165,6 +177,8 @@ class BootCfg:
         default=True, converter=value_as_boolean)
     terminal: str = "console"
     update_nvram: bool = attr.ib(default=True, converter=value_as_boolean)
+    alternatives: typing.Optional[typing.List[str]] = attr.ib(
+        validator=_check_alternatives, default=['default', 'rescue'])
 
 
 @attr.s(auto_attribs=True)
diff --git a/doc/topics/config.rst b/doc/topics/config.rst
index fde668b..7040909 100644
--- a/doc/topics/config.rst
+++ b/doc/topics/config.rst
@@ -296,7 +296,13 @@ This setting is ignored if *update_nvram* is False.
 extlinux
 """"""""
 
-There are no options specific to extlinux.
+**alternatives**: *<List of alternatives to add for each OS>*
+
+Specifies a list of alternative boot options to add for each OS. Valid values
+are:
+
+  *default*: Run with default command-line flags (ro quiet)
+  *rescue*: Run with rescue command-line flags (ro single)
 
 **Example**::
 
diff --git a/tests/unittests/test_commands_install_extlinux.py b/tests/unittests/test_commands_install_extlinux.py
index fa7dece..e914a95 100644
--- a/tests/unittests/test_commands_install_extlinux.py
+++ b/tests/unittests/test_commands_install_extlinux.py
@@ -8,10 +8,11 @@ from .helpers import CiTestCase
 
 from curtin import config
 from curtin import paths
-from curtin.commands import install_extlinux
+from curtin.commands import curthooks, install_extlinux
 
 
 USE_EXTLINUX = ['extlinux']
+ROOT_DEV = '/dev/sda1'
 
 EXPECT_HDR = '''\
 ## /boot/extlinux/extlinux.conf
@@ -27,47 +28,90 @@ prompt 0
 timeout 50
 '''
 
-EXPECT_BODY = '''
-
+EXPECT_L0 = '''
 label l0
 \tmenu label Linux 6.8.0-48-generic
-\tlinux /vmlinuz-6.8.0-48-generic
-\tinitrd /initrd.img-6.8.0-48-generic
-\tappend ro quiet
+\tlinux {fw_boot_dir}/vmlinuz-6.8.0-48-generic
+\tinitrd {fw_boot_dir}/initrd.img-6.8.0-48-generic
+\tappend {ROOT_DEV} ro quiet
+'''
 
+EXPECT_L0R = '''
 label l0r
 \tmenu label Linux 6.8.0-48-generic (rescue target)
-\tlinux /vmlinuz-6.8.0-48-generic
-\tinitrd /initrd.img-6.8.0-48-generic
-\tappend ro single
-
+\tlinux {fw_boot_dir}/vmlinuz-6.8.0-48-generic
+\tinitrd {fw_boot_dir}/initrd.img-6.8.0-48-generic
+\tappend {ROOT_DEV} ro single
+'''
 
+EXPECT_L1 = '''
 label l1
 \tmenu label Linux 6.8.0-40-generic
-\tlinux /vmlinuz-6.8.0-40-generic
-\tinitrd /initrd.img-6.8.0-40-generic
-\tappend ro quiet
+\tlinux {fw_boot_dir}/vmlinuz-6.8.0-40-generic
+\tinitrd {fw_boot_dir}/initrd.img-6.8.0-40-generic
+\tappend {ROOT_DEV} ro quiet
+'''
 
+EXPECT_L1R = '''
 label l1r
 \tmenu label Linux 6.8.0-40-generic (rescue target)
-\tlinux /vmlinuz-6.8.0-40-generic
-\tinitrd /initrd.img-6.8.0-40-generic
-\tappend ro single
-
+\tlinux {fw_boot_dir}/vmlinuz-6.8.0-40-generic
+\tinitrd {fw_boot_dir}/initrd.img-6.8.0-40-generic
+\tappend {ROOT_DEV} ro single
+'''
 
+EXPECT_L2 = '''
 label l2
 \tmenu label Linux 5.15.0-127-generic
-\tlinux /vmlinuz-5.15.0-127-generic
-\tinitrd /initrd.img-5.15.0-127-generic
-\tappend ro quiet
+\tlinux {fw_boot_dir}/vmlinuz-5.15.0-127-generic
+\tinitrd {fw_boot_dir}/initrd.img-5.15.0-127-generic
+\tappend {ROOT_DEV} ro quiet
+'''
 
+EXPECT_L2R = '''
 label l2r
 \tmenu label Linux 5.15.0-127-generic (rescue target)
-\tlinux /vmlinuz-5.15.0-127-generic
-\tinitrd /initrd.img-5.15.0-127-generic
-\tappend ro single
+\tlinux {fw_boot_dir}/vmlinuz-5.15.0-127-generic
+\tinitrd {fw_boot_dir}/initrd.img-5.15.0-127-generic
+\tappend {ROOT_DEV} ro single
 '''
 
+EXPECT_BODY = ('\n' + EXPECT_L0 + EXPECT_L0R +
+               '\n' + EXPECT_L1 + EXPECT_L1R +
+               '\n' + EXPECT_L2 + EXPECT_L2R)
+
+STORAGE = {
+    'version': 1,
+    'config': [
+        {
+            'id': 'vdb',
+            'type': 'disk',
+            'name': 'vdb',
+            'path': '/dev/vdb',
+            'ptable': 'gpt',
+        },
+        {
+            'id': 'vdb-part1',
+            'type': 'partition',
+            'device': 'vdb',
+            'number': 1,
+        },
+        {
+            'id': 'vdb-part1_format',
+            'type': 'format',
+            'volume': 'vdb-part1',
+            'fstype': 'ext4',
+        },
+        {
+            'id': 'vdb-part1_mount',
+            'type': 'mount',
+            'path': '/',
+            'device': 'vdb-part1_format',
+            'spec': ROOT_DEV,
+        },
+    ]
+}
+
 
 class TestInstallExtlinux(CiTestCase):
     def setUp(self):
@@ -87,6 +131,7 @@ class TestInstallExtlinux(CiTestCase):
         self.maxDiff = None
 
     def test_get_kernel_list(self):
+        """Check that the list of kernels is correct"""
         iter = paths.get_kernel_list(self.target, full_initrd_path=False)
         self.assertEqual(
             ('vmlinuz-6.8.0-48-generic', 'initrd.img-6.8.0-48-generic',
@@ -107,27 +152,162 @@ class TestInstallExtlinux(CiTestCase):
             pass
 
     def test_empty(self):
+        """An empty configuration with no kernels should just have a header"""
         out = install_extlinux.build_content(config.BootCfg(USE_EXTLINUX),
-                                             f'{self.target}/empty-dir')
+                                             f'{self.target}/empty-dir',
+                                             '', ROOT_DEV)
         self.assertEqual(out, EXPECT_HDR)
 
     def test_normal(self):
+        """Normal configuration, with both 'default' and 'rescue' options"""
         out = install_extlinux.build_content(config.BootCfg(USE_EXTLINUX),
-                                             self.target)
-        self.assertEqual(EXPECT_HDR + EXPECT_BODY, out)
+                                             self.target, '/boot', ROOT_DEV)
+        self.assertEqual(
+            EXPECT_HDR + EXPECT_BODY.format(fw_boot_dir='/boot',
+                                            ROOT_DEV=ROOT_DEV),
+            out)
 
-    def test_no_recovery(self):
-        out = install_extlinux.build_content(config.BootCfg(USE_EXTLINUX),
-                                             self.target)
-        self.assertEqual(EXPECT_HDR + EXPECT_BODY, out)
+    def test_no_rescue(self):
+        """Configuration with only the 'default' options"""
+        cfg = config.BootCfg(USE_EXTLINUX, alternatives=['default'])
+        out = install_extlinux.build_content(cfg, self.target, '/boot',
+                                             ROOT_DEV)
+        self.assertEqual(
+            EXPECT_HDR +
+            ('\n' + EXPECT_L0 + '\n' + EXPECT_L1 + '\n' + EXPECT_L2).format(
+                fw_boot_dir='/boot', ROOT_DEV=ROOT_DEV),
+            out)
 
-    def test_install(self):
-        install_extlinux.install_extlinux(config.BootCfg(USE_EXTLINUX),
-                                          self.target)
+    def test_no_default(self):
+        """Configuration with only the 'rescue' options"""
+        cfg = config.BootCfg(USE_EXTLINUX, alternatives=['rescue'])
+        out = install_extlinux.build_content(cfg, self.target, '/boot',
+                                             ROOT_DEV)
+        self.assertEqual(
+            EXPECT_HDR +
+            ('\n' + EXPECT_L0R + '\n' + EXPECT_L1R + '\n' + EXPECT_L2R).format(
+                fw_boot_dir='/boot', ROOT_DEV=ROOT_DEV),
+            out)
+
+    def test_separate_boot_partition(self):
+        """Check handling of a separate /boot partition"""
+        cfg = config.BootCfg(USE_EXTLINUX)
+        out = install_extlinux.build_content(cfg, self.target, '', ROOT_DEV)
+        self.assertEqual(EXPECT_HDR + EXPECT_BODY.format(
+            fw_boot_dir='', ROOT_DEV=ROOT_DEV), out)
+
+    def check_extlinux(self) -> str:
+        """Common checks for extlinux
+
+        Return: Contents of extlinux.conf
+        """
         extlinux_path = self.target + '/boot/extlinux'
         self.assertTrue(os.path.exists(extlinux_path))
         extlinux_file = extlinux_path + '/extlinux.conf'
         self.assertTrue(os.path.exists(extlinux_file))
+        with open(extlinux_file, encoding='utf-8') as inf:
+            return inf.read()
+
+    def test_install(self):
+        """Make sure the file is written to the disk"""
+        install_extlinux.install_extlinux(config.BootCfg(USE_EXTLINUX),
+                                          self.target, '/boot', ROOT_DEV)
+        out = self.check_extlinux()
+        self.assertEqual(EXPECT_HDR + EXPECT_BODY.format(
+            fw_boot_dir='/boot', ROOT_DEV=ROOT_DEV), out)
+
+    def test_install_separate_boot_partition(self):
+        """Check installation with a separate /boot partition"""
+        cfg = config.BootCfg(USE_EXTLINUX)
+        install_extlinux.install_extlinux(cfg, self.target, '', ROOT_DEV)
+        out = self.check_extlinux()
+        self.assertEqual(EXPECT_HDR + EXPECT_BODY.format
+                         (fw_boot_dir='', ROOT_DEV=ROOT_DEV), out)
+
+    def test_install_no_alternatives(self):
+        """The default in BootCfg is not used when reading a yaml file"""
+        cfg = {
+            'boot': {'bootloaders': USE_EXTLINUX},
+            'storage': STORAGE,
+        }
+        curthooks.setup_extlinux(cfg, self.target)
+        out = self.check_extlinux()
+        self.assertEqual(EXPECT_HDR + EXPECT_BODY.format(
+            fw_boot_dir='/boot', ROOT_DEV=ROOT_DEV), out)
+
+    def test_separate_boot_partition_cfg(self):
+        """setup_extlinux() should see a separate mount and set fw_boot_dir"""
+        cfg = {
+            'boot': {'bootloaders': USE_EXTLINUX},
+            'storage': {
+                'version': 1,
+                'config': [
+                    {
+                        'id': 'vdb',
+                        'type': 'disk',
+                        'name': 'vdb',
+                        'path': '/dev/vdb',
+                        'ptable': 'gpt',
+                    },
+                    {
+                        'id': 'vdb-part1',
+                        'type': 'partition',
+                        'device': 'vdb',
+                        'number': 1,
+                    },
+                    {
+                        'id': 'vdb-part2',
+                        'type': 'partition',
+                        'device': 'vdb',
+                        'flag': 'boot',
+                        'number': 2,
+                    },
+                    {
+                        'id': 'vdb-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'ext4',
+                    },
+                    {
+                        'id': 'vdb-part2_format',
+                        'type': 'format',
+                        'volume': 'vdb-part2',
+                        'fstype': 'ext4',
+                    },
+                    {
+                        'id': 'vdb-part1_mount',
+                        'type': 'mount',
+                        'path': '/',
+                        'device': 'vdb-part1_format',
+                        'spec': ROOT_DEV,
+                    },
+                    {
+                        'id': 'vdb-part2_mount',
+                        'type': 'mount',
+                        'path': '/boot',
+                        'device': 'vdb-part2_format',
+                    }
+                ]
+            }
+        }
+        curthooks.setup_extlinux(cfg, self.target)
+        out = self.check_extlinux()
+        self.assertEqual(EXPECT_HDR + EXPECT_BODY.format(
+            fw_boot_dir='', ROOT_DEV=ROOT_DEV), out)
+
+    def test_single_partition_cfg(self):
+        """setup_extlinux() should see a single mount and set fw_boot_dir"""
+        cfg = {
+            'boot': {
+                'bootloaders': USE_EXTLINUX,
+                'alternatives': ['default', 'rescue'],
+            },
+            'storage': STORAGE,
+        }
+        curthooks.setup_extlinux(cfg, self.target)
+        out = self.check_extlinux()
+        self.assertEqual(EXPECT_HDR + EXPECT_BODY.format(
+            fw_boot_dir='/boot', ROOT_DEV=ROOT_DEV), out)
 
 
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_config.py b/tests/unittests/test_config.py
index f812628..44caf51 100644
--- a/tests/unittests/test_config.py
+++ b/tests/unittests/test_config.py
@@ -11,6 +11,10 @@ from curtin import config
 from .helpers import CiTestCase
 
 
+# Selects the extlinux bootloader
+EXTLINUX = ['extlinux']
+
+
 class TestMerge(CiTestCase):
     def test_merge_cfg_string(self):
         d1 = {'str1': 'str_one'}
@@ -226,7 +230,7 @@ class TestDeserializer(CiTestCase):
             deserializer.deserialize(UnionClass, {"val": None}))
 
 
-class TestBootCfg(CiTestCase):
+class TestBootCfgBootloaders(CiTestCase):
     def test_empty(self):
         with self.assertRaises(TypeError) as exc:
             config.BootCfg()
@@ -262,4 +266,39 @@ class TestBootCfg(CiTestCase):
         config.BootCfg(['extlinux', 'grub'])
 
 
+class TestBootCfgAlternatives(CiTestCase):
+    def test_defaults(self):
+        cfg = config.BootCfg(['extlinux'])
+        self.assertEqual(['default', 'rescue'], cfg.alternatives)
+
+    def test_not_list(self):
+        with self.assertRaises(ValueError) as exc:
+            config.BootCfg(['extlinux'], alternatives='invalid')
+        self.assertIn("alternatives must be a list: invalid",
+                      str(exc.exception))
+
+    def test_empty_list(self):
+        with self.assertRaises(ValueError) as exc:
+            config.BootCfg(['extlinux'], alternatives=[])
+        self.assertIn("Empty alternatives list:", str(exc.exception))
+
+    def test_duplicate(self):
+        with self.assertRaises(ValueError) as exc:
+            config.BootCfg(['extlinux'], alternatives=['default', 'default'])
+        self.assertIn(
+            "alternatives list contains duplicates: ['default', 'default']",
+            str(exc.exception))
+
+    def test_invalid(self):
+        with self.assertRaises(ValueError) as exc:
+            config.BootCfg(['extlinux'], alternatives=['fred'])
+        self.assertIn("Unknown alternative fred: ['fred']", str(exc.exception))
+
+    def test_valid(self):
+        config.BootCfg(EXTLINUX, alternatives=['default'])
+        config.BootCfg(EXTLINUX, alternatives=['rescue'])
+        config.BootCfg(EXTLINUX, alternatives=['default', 'rescue'])
+        config.BootCfg(EXTLINUX, alternatives=['rescue', 'default'])
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
index f1bccdb..cd94c51 100644
--- a/tests/unittests/test_curthooks.py
+++ b/tests/unittests/test_curthooks.py
@@ -1479,12 +1479,45 @@ class TestSetupExtlinux(CiTestCase):
                 'bootloaders': ['extlinux'],
                 'install_devices': ['/dev/vdb'],
             },
+            'storage': {
+                'version': 1,
+                'config': [
+                    {
+                        'id': 'vdb',
+                        'type': 'disk',
+                        'name': 'vdb',
+                        'path': '/dev/vdb',
+                        'ptable': 'gpt',
+                    },
+                    {
+                        'id': 'vdb-part1',
+                        'type': 'partition',
+                        'device': 'vdb',
+                        'number': 1,
+                    },
+                    {
+                        'id': 'vdb-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'ext4',
+                    },
+                    {
+                        'id': 'vdb-part1_mount',
+                        'type': 'mount',
+                        'path': '/',
+                        'device': 'vdb-part1_format',
+                        'spec': '/dev/vdb1',
+                    },
+                ]
+            }
         }
         for machine in ['i586', 'i686', 'x86_64']:
             curthooks.setup_boot(
                 cfg, self.target, machine, '/testing',
                 osfamily=self.distro_family, variant=self.variant)
-        self.m_install_extlinux.assert_called_with(cfg, self.target)
+        bootcfg = config.fromdict(config.BootCfg, cfg['boot'])
+        self.m_install_extlinux.assert_called_with(
+            bootcfg, self.target, '/boot', '/dev/vdb1')
         self.m_setup_grub.assert_not_called()
         self.m_run_zipl.assert_not_called()
 

Follow ups