diff --git a/doc/reference/autoinstall-reference.rst b/doc/reference/autoinstall-reference.rst index a855b8662..f9703183d 100644 --- a/doc/reference/autoinstall-reference.rst +++ b/doc/reference/autoinstall-reference.rst @@ -576,6 +576,18 @@ To match a Seagate drive: match: model: Seagate +As of Subiquity 24.08.1, match specs may optionally be specified in an ordered +list, and will use the first match spec that matches one or more unused disks: + +.. code-block:: yaml + + # attempt first to match by serial, then by path + - type: disk + id: data-disk + match: + - serial: Foodisk_1TB_ABC123_1 + - path: /dev/nvme0n1 + Partition/logical volume extensions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/subiquity/models/filesystem.py b/subiquity/models/filesystem.py index d32670612..2e5930280 100644 --- a/subiquity/models/filesystem.py +++ b/subiquity/models/filesystem.py @@ -1660,14 +1660,22 @@ def _filtered_matches(self, disks: Sequence[_Device], match: dict): matchers = self._make_matchers(match) return [disk for disk in disks if all(match_fn(disk) for match_fn in matchers)] - def disk_for_match(self, disks, match): + def disk_for_match( + self, disks: Sequence[_Device], match: dict | Sequence[dict] + ) -> _Device: + # a match directive is a dict, or a list of dicts, that specify + # * zero or more keys to filter on + # * an optional sort on size log.info(f"considering {disks} for {match}") - candidates = self._filtered_matches(disks, match) - candidates = self._sorted_matches(candidates, match) - if candidates: - log.info(f"For match {match}, using the first candidate from {candidates}") - return candidates[0] - log.info(f"For match {match}, no devices match") + if isinstance(match, dict): + match = [match] + for m in match: + candidates = self._filtered_matches(disks, m) + candidates = self._sorted_matches(candidates, m) + if candidates: + log.info(f"For match {m}, using the first candidate from {candidates}") + return candidates[0] + log.info(f"No devices satisfy criteria {match}") return None def assign_omitted_offsets(self): diff --git a/subiquity/models/tests/test_filesystem.py b/subiquity/models/tests/test_filesystem.py index 2988767ec..55777a0bc 100644 --- a/subiquity/models/tests/test_filesystem.py +++ b/subiquity/models/tests/test_filesystem.py @@ -1834,3 +1834,25 @@ def test_matcher_install_media(self): fake_up_blockdata(m) actual = m.disk_for_match([iso, disk], {"install-media": True}) self.assertEqual(iso, actual) + + def test_match_from_list_first(self): + m = make_model() + vda = make_disk(m, path="/dev/vda", serial="s1") + vdb = make_disk(m, path="/dev/vdb", serial="s2") + fake_up_blockdata(m) + match = [ + {"serial": "s1"}, + {"path": "/dev/vdb"}, + ] + self.assertEqual(vda, m.disk_for_match([vda, vdb], match)) + + def test_match_from_list_second(self): + m = make_model() + vda = make_disk(m, path="/dev/vda", serial="s1") + vdb = make_disk(m, path="/dev/vdb", serial="s2") + fake_up_blockdata(m) + match = [ + {"serial": "not-found"}, + {"path": "/dev/vdb"}, + ] + self.assertEqual(vdb, m.disk_for_match([vda, vdb], match)) diff --git a/subiquity/server/controllers/filesystem.py b/subiquity/server/controllers/filesystem.py index f210267cc..2d6d8d979 100644 --- a/subiquity/server/controllers/filesystem.py +++ b/subiquity/server/controllers/filesystem.py @@ -22,7 +22,7 @@ import pathlib import subprocess import time -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Union import attr import pyudev @@ -1341,7 +1341,9 @@ async def _probe(self, *, context=None): self.start_monitor() break - def get_bootable_matching_disk(self, match: dict[str, str]): + def get_bootable_matching_disk( + self, match: dict[str, str] | Sequence[dict[str, str]] + ): """given a match directive, find disks or disk-like devices for which we have a plan to boot, and return the best matching one of those. As match directives are autoinstall-supplied, raise AutoinstallError if