From 26fde36780661257133368ac27612930c7791f2f Mon Sep 17 00:00:00 2001 From: Johnson George Date: Fri, 1 May 2026 15:18:48 -0700 Subject: [PATCH 1/2] feat(testselector): pre-filter test cases by target OS Add a distro pre-filter to select_testcases() that drops cases whose supported_os/unsupported_os makes them inapplicable to the target distro before deployment. This avoids deploying VMs only to skip unrelated cases at runtime, saving time and cost for partner runs targeting a single distro. Target OS is inferred from image variables (marketplace_image, shared_gallery_image, vhd) using a new lisa.util.os_resolver helper. The OS alias dictionary includes both distro names and publisher names (canonical, openlogic, microsoftcblmariner, etc.) so inference works from either substring. Azure blob URL domain suffixes (.blob.core.windows.net) are stripped before matching to avoid false 'windows' hits. A runbook variable 'enable_distro_pre_filtering' (default: true) controls whether the pre-filter is active. When set to false, all test cases are kept regardless of image. Compatibility uses bidirectional issubclass so a case requiring Linux still matches target Ubuntu, and a case requiring CBLMariner is kept when the target is broader. Wires the parameter through LisaRunner._initialize and the lisa list command so previewed and executed selections agree. Includes 25+ unit tests covering alias resolution, image inference (marketplace, VHD, shared gallery), hierarchy matching, and end-to-end selection. Declares supported_os or unsupported_os metadata on 15 test suites that already perform isinstance + SkippedException checks at runtime, enabling the pre-filter to act on them. In-method guards kept as defense-in-depth for cases where OS detection misclassifies the node. Suite-level additions: - cloud_hypervisor: supported_os=[CBLMariner, Ubuntu] - cvm_attestation: supported_os=[Ubuntu, CBLMariner] - cvm_boot: supported_os=[CBLMariner] - mshv_secure_boot: supported_os=[CBLMariner] - dpdk: unsupported_os=[BSD, Windows] Per-test additions in azure_image_standard: - verify_network_manager_not_installed: supported_os=[Fedora] - verify_network_file_configuration: supported_os=[Fedora, CBLMariner] - verify_ifcfg_eth0: supported_os=[Fedora] - verify_udev_rules_moved: supported_os=[CoreOs, Fedora, CBLMariner] - verify_dhcp_file_configuration: supported_os=[Suse, CBLMariner] - verify_yum_conf: supported_os=[Fedora] - verify_hv_kvp_daemon_installed: supported_os=[Debian, CBLMariner] - verify_cloud_init_error_status: supported_os=[CBLMariner] - verify_essential_kernel_modules: supported_os=[Linux] Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lisa/commands.py | 13 +- .../testsuites/cloud_hypervisor/ch_tests.py | 3 +- .../testsuites/core/azure_image_standard.py | 29 +- lisa/microsoft/testsuites/core/boot.py | 4 +- lisa/microsoft/testsuites/core/sched_core.py | 4 +- .../testsuites/cvm/cvm_attestation.py | 1 + .../testsuites/cvm/cvm_azure_host.py | 5 +- lisa/microsoft/testsuites/cvm/cvm_boot.py | 1 + lisa/microsoft/testsuites/dpdk/dpdksuite.py | 4 + .../testsuites/firewalld/firewalldsuite.py | 10 +- lisa/microsoft/testsuites/gpu/gpusuite.py | 3 + lisa/microsoft/testsuites/kdump/kdumpcrash.py | 6 +- .../testsuites/kvm/kvm_unit_tests.py | 6 +- .../testsuites/libvirt/libvirt_tck.py | 5 +- .../testsuites/mshv/mshv_root_tests.py | 5 + .../testsuites/mshv/mshv_secure_boot.py | 1 + lisa/microsoft/testsuites/power/common.py | 2 + lisa/microsoft/testsuites/power/power.py | 2 + lisa/microsoft/testsuites/power/stress.py | 2 + .../rust_vmm_mshv/rust_vmm_mshv_test.py | 7 +- .../testsuites/vm_extensions/waagent.py | 10 +- lisa/microsoft/testsuites/xdp/functional.py | 5 +- lisa/microsoft/testsuites/xdp/performance.py | 5 +- lisa/runners/lisa_runner.py | 43 ++- lisa/testselector.py | 76 +++- lisa/util/os_resolver.py | 175 +++++++++ selftests/test_distro_prefilter.py | 354 ++++++++++++++++++ 27 files changed, 753 insertions(+), 28 deletions(-) create mode 100644 lisa/util/os_resolver.py create mode 100644 selftests/test_distro_prefilter.py diff --git a/lisa/commands.py b/lisa/commands.py index 6274256c1d..e7907eebdb 100644 --- a/lisa/commands.py +++ b/lisa/commands.py @@ -13,6 +13,7 @@ from lisa.testsuite import TestCaseRuntimeData from lisa.util import LisaException, constants, hookspec, plugin_manager from lisa.util.logger import enable_console_timestamp, get_logger +from lisa.util.os_resolver import infer_target_os from lisa.util.perf_timer import create_timer _get_init_logger = functools.partial(get_logger, "init") @@ -75,12 +76,20 @@ def list_start(args: Namespace) -> int: list_all = cast(Optional[bool], args.list_all) log = _get_init_logger("list") if args.type == constants.LIST_CASE: + # Resolve target OS from runbook variables so the listing reflects + # the same distro pre-filter the runner will apply. + case_variables = {name: entry.data for name, entry in builder.variables.items()} + gate = case_variables.get("enable_distro_pre_filtering") + if gate is not None and str(gate).lower() in ("false", "0", "no"): + target_os = None + else: + target_os = infer_target_os(case_variables) if list_all: - cases: Iterable[TestCaseRuntimeData] = select_testcases() + cases: Iterable[TestCaseRuntimeData] = select_testcases(target_os=target_os) else: criteria_dict = builder.partial_resolve(constants.TESTCASE) criteria = schema.load_by_type_many(schema.TestCase, criteria_dict) - cases = select_testcases(criteria) + cases = select_testcases(criteria, target_os=target_os) for case_data in cases: log.info( f"case: {case_data.name}, suite: {case_data.metadata.suite.name}, " diff --git a/lisa/microsoft/testsuites/cloud_hypervisor/ch_tests.py b/lisa/microsoft/testsuites/cloud_hypervisor/ch_tests.py index f621c6bfa5..baec2a81eb 100644 --- a/lisa/microsoft/testsuites/cloud_hypervisor/ch_tests.py +++ b/lisa/microsoft/testsuites/cloud_hypervisor/ch_tests.py @@ -16,7 +16,7 @@ search_space, ) from lisa.operating_system import CBLMariner, Ubuntu -from lisa.testsuite import TestResult +from lisa.testsuite import TestResult, simple_requirement from lisa.tools import Dmesg, Journalctl, Ls, Lscpu, Modprobe, Usermod from lisa.util import SkippedException @@ -28,6 +28,7 @@ This test suite is for executing the tests maintained in the upstream cloud-hypervisor repo. """, + requirement=simple_requirement(supported_os=[CBLMariner, Ubuntu]), ) class CloudHypervisorTestSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: diff --git a/lisa/microsoft/testsuites/core/azure_image_standard.py b/lisa/microsoft/testsuites/core/azure_image_standard.py index e1b8a2ac58..657dabf5a3 100644 --- a/lisa/microsoft/testsuites/core/azure_image_standard.py +++ b/lisa/microsoft/testsuites/core/azure_image_standard.py @@ -435,7 +435,10 @@ def verify_grub(self, node: Node) -> None: network manager is not installed. """, priority=3, - requirement=simple_requirement(supported_platform_type=[AZURE, READY, HYPERV]), + requirement=simple_requirement( + supported_os=[Fedora], + supported_platform_type=[AZURE, READY, HYPERV], + ), ) def verify_network_manager_not_installed(self, node: Node) -> None: if isinstance(node.os, Fedora): @@ -465,7 +468,8 @@ def verify_network_manager_not_installed(self, node: Node) -> None: """, priority=1, requirement=simple_requirement( - supported_platform_type=[AZURE, READY, HYPERV, QEMU] + supported_os=[Fedora, CBLMariner], + supported_platform_type=[AZURE, READY, HYPERV, QEMU], ), ) def verify_network_file_configuration(self, node: Node) -> None: @@ -541,7 +545,8 @@ def verify_network_file_configuration(self, node: Node) -> None: """, priority=1, requirement=simple_requirement( - supported_platform_type=[AZURE, READY, HYPERV, QEMU] + supported_os=[Fedora], + supported_platform_type=[AZURE, READY, HYPERV, QEMU], ), ) def verify_ifcfg_eth0(self, node: Node) -> None: @@ -601,7 +606,8 @@ def verify_ifcfg_eth0(self, node: Node) -> None: """, priority=1, requirement=simple_requirement( - supported_platform_type=[AZURE, READY, HYPERV, QEMU] + supported_os=[CoreOs, Fedora, CBLMariner], + supported_platform_type=[AZURE, READY, HYPERV, QEMU], ), ) def verify_udev_rules_moved(self, node: Node) -> None: @@ -639,7 +645,8 @@ def verify_udev_rules_moved(self, node: Node) -> None: """, priority=1, requirement=simple_requirement( - supported_platform_type=[AZURE, READY, HYPERV, QEMU] + supported_os=[Suse, CBLMariner], + supported_platform_type=[AZURE, READY, HYPERV, QEMU], ), ) def verify_dhcp_file_configuration(self, node: Node) -> None: @@ -692,7 +699,10 @@ def verify_dhcp_file_configuration(self, node: Node) -> None: present in the file. """, priority=2, - requirement=simple_requirement(supported_platform_type=[AZURE, READY, HYPERV]), + requirement=simple_requirement( + supported_os=[Fedora], + supported_platform_type=[AZURE, READY, HYPERV], + ), ) def verify_yum_conf(self, node: Node) -> None: if isinstance(node.os, Fedora): @@ -738,6 +748,7 @@ def verify_os_update(self, node: Node) -> None: """, priority=2, requirement=simple_requirement( + supported_os=[Debian, CBLMariner], supported_platform_type=[AZURE, READY, HYPERV], supported_features=[HyperVHostType(), CvmDisabled()], ), @@ -1255,7 +1266,10 @@ def verify_boot_error_fail_warnings(self, node: Node) -> None: fail the case. """, priority=2, - requirement=simple_requirement(supported_platform_type=[AZURE, READY, HYPERV]), + requirement=simple_requirement( + supported_os=[CBLMariner], + supported_platform_type=[AZURE, READY, HYPERV], + ), ) def verify_cloud_init_error_status(self, node: Node) -> None: cat = node.tools[Cat] @@ -1789,6 +1803,7 @@ def verify_no_swap_on_osdisk(self, node: Node) -> None: cifs. """, priority=1, + requirement=simple_requirement(supported_os=[Linux]), ) def verify_essential_kernel_modules(self, node: Node) -> None: if not isinstance(node.os, Linux): diff --git a/lisa/microsoft/testsuites/core/boot.py b/lisa/microsoft/testsuites/core/boot.py index efd90d89ab..41180d17bd 100644 --- a/lisa/microsoft/testsuites/core/boot.py +++ b/lisa/microsoft/testsuites/core/boot.py @@ -42,12 +42,14 @@ class Boot(TestSuite): priority=3, requirement=simple_requirement( supported_features=[SerialConsole], + supported_os=[Redhat, CentOs], ), ) def verify_boot_with_debug_kernel( self, log: Logger, node: RemoteNode, log_path: Path ) -> None: - # 1. Skip testing if the distro is not redhat type. + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if not isinstance(node.os, Redhat) and not isinstance(node.os, CentOs): raise SkippedException( f"{node.os.name} not supported. " diff --git a/lisa/microsoft/testsuites/core/sched_core.py b/lisa/microsoft/testsuites/core/sched_core.py index dd9dc30c76..691d55a24c 100644 --- a/lisa/microsoft/testsuites/core/sched_core.py +++ b/lisa/microsoft/testsuites/core/sched_core.py @@ -24,7 +24,7 @@ TestSuiteMetadata, simple_requirement, ) -from lisa.operating_system import CBLMariner, Linux +from lisa.operating_system import CBLMariner from lisa.sut_orchestrator import AZURE, HYPERV, READY from lisa.tools import Gcc, Rm from lisa.tools.kernel_config import KernelConfig @@ -40,7 +40,7 @@ """, requirement=simple_requirement( supported_platform_type=[AZURE, READY, HYPERV], - supported_os=[Linux], + supported_os=[CBLMariner], ), ) class SchedCore(TestSuite): diff --git a/lisa/microsoft/testsuites/cvm/cvm_attestation.py b/lisa/microsoft/testsuites/cvm/cvm_attestation.py index 6a2a1ba81e..fa919f2e01 100644 --- a/lisa/microsoft/testsuites/cvm/cvm_attestation.py +++ b/lisa/microsoft/testsuites/cvm/cvm_attestation.py @@ -36,6 +36,7 @@ description=""" This test suite is for generating CVM attestation report only for azure cvms. """, + requirement=simple_requirement(supported_os=[Ubuntu, CBLMariner]), ) class AzureCVMAttestationTestSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: diff --git a/lisa/microsoft/testsuites/cvm/cvm_azure_host.py b/lisa/microsoft/testsuites/cvm/cvm_azure_host.py index 72683599d4..eefca42539 100644 --- a/lisa/microsoft/testsuites/cvm/cvm_azure_host.py +++ b/lisa/microsoft/testsuites/cvm/cvm_azure_host.py @@ -28,6 +28,7 @@ This test suite is for azure host vm pre-checks for nested-cvm cases. """, + requirement=simple_requirement(supported_os=[CBLMariner]), ) class CVMAzureHostTestSuite(TestSuite): __sev_enabled_pattern = re.compile(r"mshv: SEV-SNP is supported") @@ -37,11 +38,13 @@ class CVMAzureHostTestSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: node: Node = kwargs["node"] + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if not isinstance(node.os, (CBLMariner)): raise SkippedException( f"CVMAzureHostTestSuite is not implemented for {node.os.name}" ) - elif not is_mariner_dom0(node): + if not is_mariner_dom0(node): raise SkippedException( "CVMAzureHostTestSuite is supported only on Dom0-Mariner" ) diff --git a/lisa/microsoft/testsuites/cvm/cvm_boot.py b/lisa/microsoft/testsuites/cvm/cvm_boot.py index 94db73a96c..f74e00d516 100644 --- a/lisa/microsoft/testsuites/cvm/cvm_boot.py +++ b/lisa/microsoft/testsuites/cvm/cvm_boot.py @@ -36,6 +36,7 @@ description="""This test suite covers some common scenarios related to CVM boot on Azure. """, + requirement=simple_requirement(supported_os=[CBLMariner]), ) class CVMBootTestSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: diff --git a/lisa/microsoft/testsuites/dpdk/dpdksuite.py b/lisa/microsoft/testsuites/dpdk/dpdksuite.py index ad4012fc76..e288dbc169 100644 --- a/lisa/microsoft/testsuites/dpdk/dpdksuite.py +++ b/lisa/microsoft/testsuites/dpdk/dpdksuite.py @@ -68,6 +68,7 @@ description=""" This test suite check DPDK functionality """, + requirement=simple_requirement(unsupported_os=[BSD, Windows]), ) class Dpdk(TestSuite): # regex for parsing ring ping output for the final line, @@ -594,11 +595,14 @@ def _check_rx_or_tx_pps( min_nic_count=2, network_interface=Sriov(), unsupported_features=[Gpu, Infiniband], + unsupported_os=[CBLMariner], ), ) def verify_dpdk_vpp( self, node: Node, log: Logger, variables: Dict[str, Any] ) -> None: + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the unsupported_os gate. if isinstance(node.os, CBLMariner): raise SkippedException( UnsupportedDistroException( diff --git a/lisa/microsoft/testsuites/firewalld/firewalldsuite.py b/lisa/microsoft/testsuites/firewalld/firewalldsuite.py index cc61b50fa1..177620034a 100644 --- a/lisa/microsoft/testsuites/firewalld/firewalldsuite.py +++ b/lisa/microsoft/testsuites/firewalld/firewalldsuite.py @@ -3,7 +3,14 @@ import re from typing import Any, List -from lisa import Logger, Node, TestCaseMetadata, TestSuite, TestSuiteMetadata +from lisa import ( + Logger, + Node, + TestCaseMetadata, + TestSuite, + TestSuiteMetadata, + simple_requirement, +) from lisa.operating_system import CBLMariner from lisa.testsuite import TestResult from lisa.tools import Cat, KernelConfig, Ls @@ -125,6 +132,7 @@ def _parse_test_result( These tests interact with both iptables & nftables as backend. The testsuite is provided by the firewalld-test rpm. """, + requirement=simple_requirement(supported_os=[CBLMariner]), ) class FirewalldSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: diff --git a/lisa/microsoft/testsuites/gpu/gpusuite.py b/lisa/microsoft/testsuites/gpu/gpusuite.py index 1bce1a4437..5c321e44fd 100644 --- a/lisa/microsoft/testsuites/gpu/gpusuite.py +++ b/lisa/microsoft/testsuites/gpu/gpusuite.py @@ -47,6 +47,7 @@ description=""" This test suite runs the gpu test cases. """, + requirement=simple_requirement(supported_os=[Linux]), ) class GpuTestSuite(TestSuite): TIMEOUT = 2000 @@ -59,6 +60,8 @@ class GpuTestSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: node: Node = kwargs["node"] + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if isinstance(node.os, BSD) or isinstance(node.os, Windows): raise SkippedException(f"{node.os} is not supported.") diff --git a/lisa/microsoft/testsuites/kdump/kdumpcrash.py b/lisa/microsoft/testsuites/kdump/kdumpcrash.py index 5cfd52af77..d15ceaea72 100644 --- a/lisa/microsoft/testsuites/kdump/kdumpcrash.py +++ b/lisa/microsoft/testsuites/kdump/kdumpcrash.py @@ -14,10 +14,11 @@ node_requirement, schema, search_space, + simple_requirement, ) from lisa.features import SecurityProfile from lisa.features.security_profile import SecurityProfileSettings, SecurityProfileType -from lisa.operating_system import BSD, Windows +from lisa.operating_system import BSD, Linux, Windows from lisa.tools import KdumpCheck, Lscpu @@ -37,10 +38,13 @@ 6. crashkernel is set "auto" 7. crashkernel is set "auto" and VM has more than 2T memory """, + requirement=simple_requirement(supported_os=[Linux]), ) class KdumpCrash(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: node: Node = kwargs["node"] + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if isinstance(node.os, BSD) or isinstance(node.os, Windows): raise SkippedException(f"{node.os} is not supported.") # Skip kdump tests on CVMs (Confidential VMs) as they are not supported diff --git a/lisa/microsoft/testsuites/kvm/kvm_unit_tests.py b/lisa/microsoft/testsuites/kvm/kvm_unit_tests.py index 0403658fe9..cd762462b2 100644 --- a/lisa/microsoft/testsuites/kvm/kvm_unit_tests.py +++ b/lisa/microsoft/testsuites/kvm/kvm_unit_tests.py @@ -7,7 +7,7 @@ from lisa import Logger, Node, TestCaseMetadata, TestSuite, TestSuiteMetadata from lisa.operating_system import BSD, CBLMariner, Ubuntu, Windows -from lisa.testsuite import TestResult +from lisa.testsuite import TestResult, simple_requirement from lisa.tools import Lscpu from lisa.util import SkippedException @@ -19,10 +19,13 @@ This test suite is for executing the community maintained KVM tests. See: https://gitlab.com/kvm-unit-tests/kvm-unit-tests """, + requirement=simple_requirement(supported_os=[CBLMariner, Ubuntu]), ) class KvmUnitTestSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: node: Node = kwargs["node"] + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if isinstance(node.os, BSD) or isinstance(node.os, Windows): raise SkippedException(f"{node.os} is not supported.") @@ -42,6 +45,7 @@ def verify_kvm_unit_tests( virtualization_enabled = node.tools[Lscpu].is_virtualization_enabled() if not virtualization_enabled: raise SkippedException("Virtualization is not enabled in hardware") + # Defense-in-depth: same rationale as the before_case check above. if not isinstance(node.os, (CBLMariner, Ubuntu)): raise SkippedException( f"KVM unit tests are not implemented in LISA for {node.os.name}" diff --git a/lisa/microsoft/testsuites/libvirt/libvirt_tck.py b/lisa/microsoft/testsuites/libvirt/libvirt_tck.py index 4372fd8e3c..477495e982 100644 --- a/lisa/microsoft/testsuites/libvirt/libvirt_tck.py +++ b/lisa/microsoft/testsuites/libvirt/libvirt_tck.py @@ -7,7 +7,7 @@ from lisa import Logger, Node, TestCaseMetadata, TestSuite, TestSuiteMetadata from lisa.operating_system import CBLMariner, Ubuntu -from lisa.testsuite import TestResult +from lisa.testsuite import TestResult, simple_requirement from lisa.tools import Dmesg, Journalctl, Lscpu from lisa.util import SkippedException @@ -22,10 +22,13 @@ More info: https://gitlab.com/libvirt/libvirt-tck/-/blob/master/README.rst """, + requirement=simple_requirement(supported_os=[Ubuntu, CBLMariner]), ) class LibvirtTckSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: node = kwargs["node"] + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if not isinstance(node.os, (Ubuntu, CBLMariner)): raise SkippedException( f"Libvirt TCK suite is not implemented in LISA for {node.os.name}" diff --git a/lisa/microsoft/testsuites/mshv/mshv_root_tests.py b/lisa/microsoft/testsuites/mshv/mshv_root_tests.py index db675e3f32..527b65a904 100644 --- a/lisa/microsoft/testsuites/mshv/mshv_root_tests.py +++ b/lisa/microsoft/testsuites/mshv/mshv_root_tests.py @@ -14,6 +14,7 @@ TestCaseMetadata, TestSuite, TestSuiteMetadata, + simple_requirement, ) from lisa.operating_system import CBLMariner from lisa.testsuite import TestResult @@ -40,6 +41,7 @@ Microsoft Hypervisor (MSHV) root partition. This test suite contains tests to check health of mshv root node. """, + requirement=simple_requirement(supported_os=[CBLMariner]), ) class MshvHostTestSuite(TestSuite): mshvdiag_dmesg_pattern = re.compile(r"\[\s+\d+.\d+\]\s+mshv_diag:.*$") @@ -130,6 +132,9 @@ def verify_mshv_crash( mshv_debug_sysfs = "/sys/kernel/debug/mshv/hvdbg" # sysfs entry expect 0x4856434f5245 value to trigger crash from hv mshv_crash_command = f"echo 0x4856434f5245 > {mshv_debug_sysfs}" + + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if not isinstance(node.os, CBLMariner): raise SkippedException( f"Testcase only support CBLMariner. Found: {node.os}" diff --git a/lisa/microsoft/testsuites/mshv/mshv_secure_boot.py b/lisa/microsoft/testsuites/mshv/mshv_secure_boot.py index 016bb6632f..c0dd00c97e 100644 --- a/lisa/microsoft/testsuites/mshv/mshv_secure_boot.py +++ b/lisa/microsoft/testsuites/mshv/mshv_secure_boot.py @@ -35,6 +35,7 @@ description="""This test suite covers the secure boot flow for Dom0 AzureLinux nodes. """, + requirement=simple_requirement(supported_os=[CBLMariner]), ) class Dom0SecureBootTestSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: diff --git a/lisa/microsoft/testsuites/power/common.py b/lisa/microsoft/testsuites/power/common.py index 947b85fd7e..c1f5642559 100644 --- a/lisa/microsoft/testsuites/power/common.py +++ b/lisa/microsoft/testsuites/power/common.py @@ -117,6 +117,8 @@ def hibernation_before_case(node: Node, log: Logger) -> None: Common before_case logic for hibernation tests. Validates OS support and prepares the environment. """ + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if isinstance(node.os, BSD) or isinstance(node.os, Windows): raise SkippedException(f"{node.os} is not supported.") diff --git a/lisa/microsoft/testsuites/power/power.py b/lisa/microsoft/testsuites/power/power.py index 5d0418f4db..71d5c2c9a1 100644 --- a/lisa/microsoft/testsuites/power/power.py +++ b/lisa/microsoft/testsuites/power/power.py @@ -27,6 +27,7 @@ from lisa.features import Disk, HibernationEnabled, Sriov, Synthetic from lisa.features.availability import AvailabilityTypeNoRedundancy from lisa.node import Node +from lisa.operating_system import Linux from lisa.search_space import IntRange from lisa.sut_orchestrator.azure.features import AzureExtension from lisa.testsuite import simple_requirement @@ -40,6 +41,7 @@ description=""" This test suite is to test hibernation in guest VM. """, + requirement=simple_requirement(supported_os=[Linux]), ) class Power(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: diff --git a/lisa/microsoft/testsuites/power/stress.py b/lisa/microsoft/testsuites/power/stress.py index 3d395776f4..2079f7304f 100644 --- a/lisa/microsoft/testsuites/power/stress.py +++ b/lisa/microsoft/testsuites/power/stress.py @@ -21,6 +21,7 @@ from lisa.features import HibernationEnabled, Sriov from lisa.features.availability import AvailabilityTypeNoRedundancy from lisa.node import Node +from lisa.operating_system import Linux from lisa.testsuite import simple_requirement @@ -30,6 +31,7 @@ description=""" This test suite is to test hibernation in guest vm under stress. """, + requirement=simple_requirement(supported_os=[Linux]), ) class PowerStress(TestSuite): _loop = 10 diff --git a/lisa/microsoft/testsuites/rust_vmm_mshv/rust_vmm_mshv_test.py b/lisa/microsoft/testsuites/rust_vmm_mshv/rust_vmm_mshv_test.py index ea42a810e5..bc10fe39e1 100644 --- a/lisa/microsoft/testsuites/rust_vmm_mshv/rust_vmm_mshv_test.py +++ b/lisa/microsoft/testsuites/rust_vmm_mshv/rust_vmm_mshv_test.py @@ -8,8 +8,8 @@ from lisa import Logger, Node, TestCaseMetadata, TestSuite, TestSuiteMetadata from lisa.messages import TestStatus, send_sub_test_result_message -from lisa.operating_system import BSD, Windows -from lisa.testsuite import TestResult +from lisa.operating_system import BSD, Linux, Windows +from lisa.testsuite import TestResult, simple_requirement from lisa.tools import Cargo, Dmesg, Git, Ls, RemoteCopy from lisa.util import SkippedException from lisa.util.process import ExecutableResult @@ -21,10 +21,13 @@ description=""" This test suite is for executing the rust-vmm/mshv tests """, + requirement=simple_requirement(supported_os=[Linux]), ) class RustVmmTestSuite(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: node: Node = kwargs["node"] + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if isinstance(node.os, BSD) or isinstance(node.os, Windows): raise SkippedException(f"{node.os} is not supported.") diff --git a/lisa/microsoft/testsuites/vm_extensions/waagent.py b/lisa/microsoft/testsuites/vm_extensions/waagent.py index 0711c5da0b..d5d63c7fda 100644 --- a/lisa/microsoft/testsuites/vm_extensions/waagent.py +++ b/lisa/microsoft/testsuites/vm_extensions/waagent.py @@ -32,11 +32,15 @@ class WaAgentBvt(TestSuite): remote machine. """, priority=1, - requirement=simple_requirement(supported_features=[AzureExtension]), + requirement=simple_requirement( + supported_features=[AzureExtension], + unsupported_os=[FreeBSD], + ), ) def verify_vm_agent(self, log: Logger, node: Node) -> None: - # Some of the most common extensions, including Custom Script, are - # not supported on FreeBSD so skip the test on that case. + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the unsupported_os gate. + # Custom Script and most common extensions are not supported on FreeBSD. if isinstance(node.os, FreeBSD): raise SkippedException(f"unsupported distro type: {type(node.os)}") diff --git a/lisa/microsoft/testsuites/xdp/functional.py b/lisa/microsoft/testsuites/xdp/functional.py index edde4ca6ac..bd973c926c 100644 --- a/lisa/microsoft/testsuites/xdp/functional.py +++ b/lisa/microsoft/testsuites/xdp/functional.py @@ -22,7 +22,7 @@ simple_requirement, ) from lisa.features import NetworkInterface, Sriov, Synthetic -from lisa.operating_system import BSD, Windows +from lisa.operating_system import BSD, Linux, Windows from lisa.tools import Firewall, Ip, Kill, TcpDump from lisa.tools.ping import INTERNET_PING_ADDRESS from lisa.util import get_matched_str @@ -35,6 +35,7 @@ description=""" This test suite is to validate XDP functionality. """, + requirement=simple_requirement(supported_os=[Linux]), ) class XdpFunctional(TestSuite): # sample output: @@ -45,6 +46,8 @@ class XdpFunctional(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: node: Node = kwargs["node"] environment: Environment = kwargs.pop("environment") + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if isinstance(node.os, BSD) or isinstance(node.os, Windows): raise SkippedException(f"XDP is not supported in {node.os.name} yet.") diff --git a/lisa/microsoft/testsuites/xdp/performance.py b/lisa/microsoft/testsuites/xdp/performance.py index 4001f473a8..85e43501f0 100644 --- a/lisa/microsoft/testsuites/xdp/performance.py +++ b/lisa/microsoft/testsuites/xdp/performance.py @@ -35,7 +35,7 @@ from lisa.executable import Tool from lisa.features import Sriov, Synthetic from lisa.nic import NicInfo -from lisa.operating_system import BSD, Windows +from lisa.operating_system import BSD, Linux, Windows from lisa.testsuite import TestResult from lisa.tools import Firewall, Kill, Lagscope, Lscpu, Ntttcp from lisa.util.parallel import run_in_parallel @@ -52,11 +52,14 @@ description=""" This test suite is to validate XDP performance. """, + requirement=simple_requirement(supported_os=[Linux]), ) class XdpPerformance(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: environment: Environment = kwargs.pop("environment") node: Node = kwargs["node"] + # Defense-in-depth: catches custom VHD/SIG images whose OS detection + # may misclassify the node and bypass the supported_os gate. if isinstance(node.os, BSD) or isinstance(node.os, Windows): raise SkippedException(f"{node.os} is not supported.") diff --git a/lisa/runners/lisa_runner.py b/lisa/runners/lisa_runner.py index da4b11c26c..d04873b970 100644 --- a/lisa/runners/lisa_runner.py +++ b/lisa/runners/lisa_runner.py @@ -37,10 +37,35 @@ deep_update_dict, is_unittest, ) +from lisa.util.os_resolver import infer_target_os from lisa.util.parallel import Task, check_cancelled from lisa.variable import VariableEntry +def _resolve_target_os( + variables: Any, +) -> Optional[type]: + """Return the target OS class if distro pre-filtering is enabled. + + The ``enable_distro_pre_filtering`` runbook variable (default ``true``) + controls whether the pre-filter is active. When set to ``false`` the + function returns ``None`` and all test cases are kept. + """ + # Accept both raw dicts and VariableEntry-style mappings. + if isinstance(variables, dict): + raw = variables + else: + raw = {} + # Check for the gate variable. Treat missing / empty as "true". + gate = raw.get("enable_distro_pre_filtering") + if gate is not None: + # VariableEntry wraps the real value in .data; plain dicts don't. + val = getattr(gate, "data", gate) + if str(val).lower() in ("false", "0", "no"): + return None + return infer_target_os(variables) + + class LisaRunner(BaseRunner): @classmethod def type_name(cls) -> str: @@ -49,8 +74,22 @@ def type_name(cls) -> str: def _initialize(self, *args: Any, **kwargs: Any) -> None: super()._initialize(*args, **kwargs) - # select test cases - selected_test_cases = select_testcases(filters=self._runbook.testcase) + # select test cases. When the user specifies a target_os variable, or + # when one can be inferred from a marketplace / gallery / vhd image + # variable, drop cases whose declared supported_os/unsupported_os + # requirement makes them inapplicable to that distro. This avoids the + # overhead of deploying an environment just to mark cases as Skipped. + # Use the full runbook variable pool (not ``self._case_variables``, + # which only contains ``is_case_visible=True`` entries) so the + # prefilter sees standard runbook variables like ``marketplace_image`` + # and ``target_os`` even when they are not marked case-visible. + variables_pool = ( + getattr(self._runbook_builder, "variables", None) or self._case_variables + ) + target_os = _resolve_target_os(variables_pool) + selected_test_cases = select_testcases( + filters=self._runbook.testcase, target_os=target_os + ) # create test results self.test_results = [ diff --git a/lisa/testselector.py b/lisa/testselector.py index a9921100c2..913d001837 100644 --- a/lisa/testselector.py +++ b/lisa/testselector.py @@ -3,9 +3,21 @@ import re from functools import partial -from typing import Callable, Dict, List, Mapping, Optional, Pattern, Set, Union, cast +from typing import ( + Callable, + Dict, + List, + Mapping, + Optional, + Pattern, + Set, + Type, + Union, + cast, +) from lisa import schema +from lisa.operating_system import OperatingSystem from lisa.testsuite import TestCaseMetadata, TestCaseRuntimeData, get_cases_metadata from lisa.util import LisaException, constants, set_filtered_fields from lisa.util.logger import get_logger @@ -13,12 +25,72 @@ _get_logger = partial(get_logger, "init", "selector") +def _is_os_compatible(case: TestCaseMetadata, target_os: Type[OperatingSystem]) -> bool: + """Return True if the case's declared OS requirement is compatible with + the target OS class. The check uses bidirectional ``issubclass`` so that + a case requiring a broad family (e.g. ``Linux``) accepts a specific + target (``Ubuntu``), and a case requiring a specific OS (``CBLMariner``) + is also kept when the target is broader (``Linux``). + """ + requirement = getattr(case, "requirement", None) + if requirement is None: + return True + os_type = getattr(requirement, "os_type", None) + if not os_type or len(os_type) == 0: + return True + + matched = False + for allowed in os_type: + # Skip non-class entries defensively; SetSpace items should always be + # OperatingSystem subclasses but we don't want a stray value to crash + # the entire selection. + if not isinstance(allowed, type): + continue + if issubclass(target_os, allowed) or issubclass(allowed, target_os): + matched = True + break + + if os_type.is_allow_set: + return matched + return not matched + + +def _prefilter_by_target_os( + full_list: Dict[str, TestCaseMetadata], target_os: Type[OperatingSystem] +) -> Dict[str, TestCaseMetadata]: + """Drop cases from ``full_list`` whose ``requirement.os_type`` declares + they cannot run on ``target_os``. Logs a one-line summary so partners + can see how many cases were skipped at selection time. + """ + log = _get_logger() + kept: Dict[str, TestCaseMetadata] = {} + dropped_names: List[str] = [] + for name, metadata in full_list.items(): + if _is_os_compatible(metadata, target_os): + kept[name] = metadata + else: + dropped_names.append(name) + if dropped_names: + log.info( + f"pre-filter: dropped {len(dropped_names)} case(s) incompatible " + f"with target_os={target_os.__name__}" + ) + log.debug(f"pre-filter dropped cases: {dropped_names}") + return kept + + def select_testcases( filters: Optional[List[schema.TestCase]] = None, init_cases: Optional[List[TestCaseMetadata]] = None, + target_os: Optional[Type[OperatingSystem]] = None, ) -> List[TestCaseRuntimeData]: """ based on filters to select test cases. If filters are None, return all cases. + + When ``target_os`` is provided, cases whose declared ``supported_os`` / + ``unsupported_os`` requirement makes them inapplicable to that OS class + are dropped before any other filter runs. This avoids deploying + environments only to mark unrelated cases as Skipped at runtime. """ log = _get_logger() if init_cases: @@ -27,6 +99,8 @@ def select_testcases( full_list[item.full_name] = item else: full_list = get_cases_metadata() + if target_os is not None: + full_list = _prefilter_by_target_os(full_list, target_os) if filters: selected: Dict[str, TestCaseRuntimeData] = {} force_included: Set[str] = set() diff --git a/lisa/util/os_resolver.py b/lisa/util/os_resolver.py new file mode 100644 index 0000000000..1c904435dd --- /dev/null +++ b/lisa/util/os_resolver.py @@ -0,0 +1,175 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +""" +Helpers to resolve a target operating system class from a free-form name or +from runbook variables. Used by the test selector to pre-filter test cases +that are not applicable to the target distro, avoiding the overhead of +deploying environments only to skip the cases at runtime. +""" + +import re +from typing import Any, Dict, Optional, Type + +from lisa.operating_system import OperatingSystem +from lisa.util.logger import get_logger + +_log = get_logger("init", "os_resolver") + +# Known short names / aliases that map to a concrete OperatingSystem subclass. +# Keys are normalized (lowercased, no separators); values are class names that +# must exist in lisa.operating_system. The map intentionally favors common +# distro brand names and acronyms over exact class names so users do not need +# to know LISA's internal naming. +_OS_ALIASES: Dict[str, str] = { + # Debian family + "ubuntu": "Ubuntu", + "canonical": "Ubuntu", + "debian": "Debian", + # Red Hat family + "rhel": "Redhat", + "redhat": "Redhat", + "centos": "CentOs", + "openlogic": "CentOs", + "oracle": "Oracle", + "ol": "Oracle", + "almalinux": "AlmaLinux", + "alma": "AlmaLinux", + # SUSE family + "suse": "Suse", + "sles": "SLES", + "opensuse": "Suse", + # Fedora + "fedora": "Fedora", + # Azure Linux / CBL-Mariner (same product, multiple brand names) + "azurelinux": "CBLMariner", + "azlinux": "CBLMariner", + "azl": "CBLMariner", + "mariner": "CBLMariner", + "cblmariner": "CBLMariner", + "microsoftcblmariner": "CBLMariner", + # BSD family + "freebsd": "FreeBSD", + "microsoftcbsd": "FreeBSD", + "openbsd": "OpenBSD", + "bsd": "BSD", + # Other + "alpine": "Alpine", + "coreos": "CoreOs", + "flatcar": "CoreOs", + "kinvolk": "CoreOs", + "linux": "Linux", + "windows": "Windows", +} + +# Variable keys that may carry an image string from which the distro can be +# inferred. +_IMAGE_VAR_KEYS = ( + "marketplace_image", + "shared_gallery_image", + "community_gallery_image", + "vhd", + "image", +) + + +def _normalize(name: str) -> str: + """Lowercase and strip non-alphanumerics so 'CBL-Mariner', 'cbl_mariner' + and 'cblmariner' all resolve identically.""" + return re.sub(r"[^a-z0-9]+", "", name.lower()) + + +def _all_os_subclasses() -> Dict[str, Type[OperatingSystem]]: + """Walk the OperatingSystem class tree and return {normalized_name: cls}.""" + found: Dict[str, Type[OperatingSystem]] = {} + stack = [OperatingSystem] + while stack: + cls = stack.pop() + found[_normalize(cls.__name__)] = cls + stack.extend(cls.__subclasses__()) + return found + + +def resolve_os_class(name: Optional[str]) -> Optional[Type[OperatingSystem]]: + """Map a string like 'ubuntu' or 'azurelinux' to an OperatingSystem + subclass. Returns None when the name is empty, unknown, or refers to a + class that no longer exists. + """ + if not name: + return None + + normalized = _normalize(name) + if not normalized: + return None + + # First consult the alias map, then fall back to a direct class-name match. + target_class_name = _OS_ALIASES.get(normalized) + candidates = _all_os_subclasses() + if target_class_name: + cls = candidates.get(_normalize(target_class_name)) + if cls is not None: + return cls + + return candidates.get(normalized) + + +def _infer_from_image_string(image: str) -> Optional[Type[OperatingSystem]]: + """Best-effort inference of an OS class from an image identifier + (marketplace 'publisher offer sku version' string, gallery name, vhd path, + etc.). Matches the longest alias substring found in the lowercased image + text to avoid false hits on short tokens. + """ + if not image: + return None + # Strip common Azure URL domain suffixes that contain OS-like substrings + # (e.g. '.windows.net', 'blob.core.windows.net') before matching. + text = re.sub( + r"(\.blob\.core\.windows\.net|\.windows\.net|\.azure\.com)", "", image.lower() + ) + # Normalize text (remove non-alphanumeric) so aliases with separators + # (e.g. 'cbl-mariner' vs 'cblmariner') match reliably. + normalized_text = re.sub(r"[^a-z0-9]+", "", text) + # Sort aliases by length (longest first) so 'cblmariner' wins over + # 'mariner' if both happen to be present, and 'almalinux' wins over 'alma'. + for alias in sorted(_OS_ALIASES.keys(), key=len, reverse=True): + if alias in normalized_text: + cls = resolve_os_class(alias) + if cls is not None: + return cls + return None + + +def infer_target_os( + variables: Optional[Dict[str, Any]], +) -> Optional[Type[OperatingSystem]]: + """Infer the target OS from image-related runbook variables. + + Checks common image variable keys (marketplace, gallery, vhd) and + extracts the distro from the image string. Returns None when no + image variable is set or the distro cannot be determined — the + caller should treat None as 'no pre-filter'. + """ + if not variables: + return None + + def _unwrap(raw: Any) -> Any: + # Runners pass ``Dict[str, VariableEntry]`` while the list/CLI path + # passes ``Dict[str, Any]`` of already-unwrapped values. Accept both + # by duck-typing on the ``data`` attribute that ``VariableEntry`` + # exposes, without importing the variable module here. + return getattr(raw, "data", raw) + + # Infer from any provided image identifier. + for key in _IMAGE_VAR_KEYS: + value = _unwrap(variables.get(key)) + if isinstance(value, str) and value.strip(): + cls = _infer_from_image_string(value) + if cls is not None: + _log.info( + f"target_os inferred as '{cls.__name__}' " + f"(source: variable '{key}'='{value}')" + ) + return cls + + # Nothing to go on. + return None diff --git a/selftests/test_distro_prefilter.py b/selftests/test_distro_prefilter.py new file mode 100644 index 0000000000..a217560d24 --- /dev/null +++ b/selftests/test_distro_prefilter.py @@ -0,0 +1,354 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Any, Dict, List +from unittest import TestCase + +from lisa.operating_system import ( + BSD, + SLES, + AlmaLinux, + CBLMariner, + CentOs, + CoreOs, + Debian, + Fedora, + FreeBSD, + Linux, + Oracle, + Redhat, + Suse, + Ubuntu, + Windows, +) +from lisa.testselector import _is_os_compatible, select_testcases +from lisa.testsuite import TestCaseMetadata, TestSuiteMetadata, simple_requirement +from lisa.util.os_resolver import ( + _infer_from_image_string, + infer_target_os, + resolve_os_class, +) + +# A standalone TestSuiteMetadata used as the .suite reference on every mock +# case. We construct it directly (without invoking it as a decorator) so the +# global suite/case registry stays untouched and tests do not leak metadata +# to each other. +_MOCK_SUITE = TestSuiteMetadata( + area="a_prefilter", + category="c_prefilter", + description="prefilter mock suite", + tags=[], + name="PrefilterMock", +) + + +def _build_case( + name: str, + *, + supported_os: Any = None, + unsupported_os: Any = None, +) -> TestCaseMetadata: + """Construct a TestCaseMetadata with the requested OS requirement, + bypassing the global registry. The returned object can be passed to + ``select_testcases(init_cases=[...])`` directly. + """ + requirement = simple_requirement( + supported_os=supported_os, unsupported_os=unsupported_os + ) + metadata = TestCaseMetadata( + description=f"des_{name}", priority=2, requirement=requirement + ) + # Mimic the attributes that __call__ would set if this were used as a + # real decorator. select_testcases keys cases by full_name and logs via + # metadata.suite.area / .category, so both are required. + metadata.name = name + metadata.full_name = f"{_MOCK_SUITE.name}.{name}" + metadata.suite = _MOCK_SUITE + metadata.tags = [] + return metadata + + +class ResolveOsClassTestCase(TestCase): + def test_resolve_known_aliases(self) -> None: + cases = { + # Debian family (distro + publisher) + "ubuntu": Ubuntu, + "Ubuntu": Ubuntu, + "canonical": Ubuntu, + "Canonical": Ubuntu, + "debian": Debian, + "Debian": Debian, + # Red Hat family (distro + publisher) + "rhel": Redhat, + "redhat": Redhat, + "RedHat": Redhat, + "centos": CentOs, + "CentOS": CentOs, + "openlogic": CentOs, + "OpenLogic": CentOs, + "oracle": Oracle, + "ol": Oracle, + "almalinux": AlmaLinux, + "alma": AlmaLinux, + # SUSE family + "suse": Suse, + "SUSE": Suse, + "sles": SLES, + "opensuse": Suse, + # Fedora + "fedora": Fedora, + # Azure Linux / CBL-Mariner (distro + publisher) + "azurelinux": CBLMariner, + "azl": CBLMariner, + "mariner": CBLMariner, + "cbl-mariner": CBLMariner, + "CBL_Mariner": CBLMariner, + "cblmariner": CBLMariner, + "microsoftcblmariner": CBLMariner, + "MicrosoftCBLMariner": CBLMariner, + # BSD family (distro + publisher) + "freebsd": FreeBSD, + "FreeBSD": FreeBSD, + "microsoftcbsd": FreeBSD, + "bsd": BSD, + # Other + "coreos": CoreOs, + "flatcar": CoreOs, + "kinvolk": CoreOs, + "linux": Linux, + "windows": Windows, + } + for name, expected in cases.items(): + self.assertIs(resolve_os_class(name), expected, msg=f"name={name}") + + def test_resolve_class_name_directly(self) -> None: + self.assertIs(resolve_os_class("Debian"), Debian) + self.assertIs(resolve_os_class("Linux"), Linux) + + def test_resolve_unknown_returns_none(self) -> None: + self.assertIsNone(resolve_os_class("notadistro")) + self.assertIsNone(resolve_os_class("")) + self.assertIsNone(resolve_os_class(None)) + + +class InferFromImageTestCase(TestCase): + def test_infer_marketplace_strings(self) -> None: + cases = { + # Ubuntu + "Canonical 0001-com-ubuntu-server-jammy 22_04-lts latest": Ubuntu, + "canonical 0001-com-ubuntu-server-focal 20_04-lts-gen2 latest": Ubuntu, + "canonical ubuntuserver 18.04-lts latest": Ubuntu, + # Debian + "Debian debian-12 12 latest": Debian, + "debian debian-11 11-gen2 latest": Debian, + # Red Hat + "RedHat RHEL 9-lvm-gen2 latest": Redhat, + "redhat rhel 8-lbr-gen2 latest": Redhat, + "redhat rhel-byos 8_4 latest": Redhat, + # CentOS + "OpenLogic CentOS 7_9-gen2 latest": CentOs, + "openlogic centos-hpc 7.6 latest": CentOs, + # Oracle + "Oracle Oracle-Linux ol79-gen2 latest": Oracle, + "oracle oracle-linux ol88-lvm-gen2 latest": Oracle, + # AlmaLinux + "almalinux almalinux 8-gen2 latest": AlmaLinux, + "almalinux almalinux-x86_64 9-gen2 latest": AlmaLinux, + # SUSE / SLES (publisher 'suse' matches the Suse alias) + "SUSE sles-15-sp5 gen2 latest": Suse, + "suse sles-byos 12-sp5-gen2 latest": Suse, + "suse opensuse-leap-15-5 gen2 latest": Suse, + # Fedora + "fedora fedora-coreos stable latest": Fedora, + # CBL-Mariner / Azure Linux + "MicrosoftCBLMariner cbl-mariner 2-gen2 latest": CBLMariner, + "microsoftcblmariner cbl-mariner cbl-mariner-2 gen2": CBLMariner, + "microsoftcblmariner azurelinux-3 3-gen2 latest": CBLMariner, + # FreeBSD + "MicrosoftCBSD FreeBSD 13.2 latest": FreeBSD, + # CoreOS / Flatcar + "kinvolk flatcar-container-linux-free stable-gen2 latest": CoreOs, + } + for image, expected in cases.items(): + self.assertIs( + _infer_from_image_string(image), expected, msg=f"image={image}" + ) + + def test_infer_returns_none_for_opaque_strings(self) -> None: + self.assertIsNone(_infer_from_image_string("")) + self.assertIsNone(_infer_from_image_string("private-image-v1")) + + def test_infer_vhd_strings(self) -> None: + cases = { + "https://storage.blob.core.windows.net/vhds/ubuntu-22.04.vhd": Ubuntu, + "https://storage.blob.core.windows.net/vhds/rhel-9.2-gen2.vhd": Redhat, + "/subscriptions/.../images/azurelinux-3.0.vhd": CBLMariner, + "https://sa.blob.core.windows.net/images/debian-12.vhd": Debian, + "/path/to/sles-15-sp5.vhd": SLES, + "https://sa.blob.core.windows.net/vhds/custom-image-v1.vhd": None, + } + for vhd, expected in cases.items(): + result = _infer_from_image_string(vhd) + self.assertIs( + result, expected, msg=f"vhd={vhd}, got={result}, expected={expected}" + ) + + def test_infer_shared_gallery_strings(self) -> None: + cases = { + "/galleries/myGallery/images/ubuntu-22.04-gen2/versions/1.0.0": Ubuntu, + "/galleries/myGallery/images/mariner-2-gen2/versions/latest": CBLMariner, + "/galleries/testGallery/images/rhel-9-lvm/versions/2.0.0": Redhat, + } + for gallery, expected in cases.items(): + result = _infer_from_image_string(gallery) + self.assertIs(result, expected, msg=f"gallery={gallery}") + + +class InferTargetOsTestCase(TestCase): + def test_infers_from_marketplace_image(self) -> None: + variables: Dict[str, Any] = { + "marketplace_image": ( + "Canonical 0001-com-ubuntu-server-jammy 22_04-lts latest" + ), + } + self.assertIs(infer_target_os(variables), Ubuntu) + + def test_returns_none_when_no_hints(self) -> None: + self.assertIsNone(infer_target_os(None)) + self.assertIsNone(infer_target_os({})) + self.assertIsNone(infer_target_os({"some_unrelated_var": "value"})) + + def test_returns_none_for_opaque_image(self) -> None: + variables: Dict[str, Any] = { + "marketplace_image": "private-image-v1", + } + self.assertIsNone(infer_target_os(variables)) + + def test_unwraps_variable_entry_objects(self) -> None: + # Runners pass ``Dict[str, VariableEntry]``; the resolver must read + # the wrapped ``data`` attribute, not the entry object itself. + from lisa.variable import VariableEntry + + variables: Dict[str, Any] = { + "marketplace_image": VariableEntry( + name="marketplace_image", + data="RedHat RHEL 9_4 latest", + ), + } + self.assertIs(infer_target_os(variables), Redhat) + + variables = { + "marketplace_image": VariableEntry( + name="marketplace_image", + data="Canonical 0001-com-ubuntu-server-jammy 22_04-lts latest", + ), + } + self.assertIs(infer_target_os(variables), Ubuntu) + + +class IsOsCompatibleTestCase(TestCase): + def test_no_requirement_keeps_case(self) -> None: + case = _build_case("any_distro") + self.assertTrue(_is_os_compatible(case, Ubuntu)) + self.assertTrue(_is_os_compatible(case, CBLMariner)) + # Default unsupported_os=[Windows] is injected when both are None. + self.assertFalse(_is_os_compatible(case, Windows)) + + def test_specific_supported_os_keeps_only_matching_target(self) -> None: + case = _build_case("mariner_only", supported_os=[CBLMariner]) + self.assertTrue(_is_os_compatible(case, CBLMariner)) + self.assertFalse(_is_os_compatible(case, Ubuntu)) + self.assertFalse(_is_os_compatible(case, Redhat)) + + def test_broad_supported_os_keeps_specific_target(self) -> None: + case = _build_case("any_linux", supported_os=[Linux]) + self.assertTrue(_is_os_compatible(case, Ubuntu)) + self.assertTrue(_is_os_compatible(case, CBLMariner)) + + def test_specific_supported_os_keeps_broad_target(self) -> None: + case = _build_case("mariner_only", supported_os=[CBLMariner]) + self.assertTrue(_is_os_compatible(case, Linux)) + + def test_unsupported_os_drops_target(self) -> None: + case = _build_case("not_ubuntu", unsupported_os=[Ubuntu]) + self.assertFalse(_is_os_compatible(case, Ubuntu)) + self.assertTrue(_is_os_compatible(case, CBLMariner)) + + def test_unsupported_family_drops_descendants(self) -> None: + case = _build_case("not_debian", unsupported_os=[Debian]) + self.assertFalse(_is_os_compatible(case, Ubuntu)) + self.assertTrue(_is_os_compatible(case, CBLMariner)) + + +class SelectTestcasesPrefilterTestCase(TestCase): + def _generate_mixed_cases(self) -> List[TestCaseMetadata]: + return [ + _build_case("any_linux"), + _build_case("ubuntu_only", supported_os=[Ubuntu]), + _build_case("mariner_only", supported_os=[CBLMariner]), + _build_case("not_ubuntu", unsupported_os=[Ubuntu]), + ] + + def test_no_target_os_keeps_all_cases(self) -> None: + cases = self._generate_mixed_cases() + results = select_testcases(filters=None, init_cases=cases, target_os=None) + names = sorted(r.name for r in results) + self.assertEqual( + names, ["any_linux", "mariner_only", "not_ubuntu", "ubuntu_only"] + ) + + def test_target_ubuntu_drops_mariner_and_not_ubuntu(self) -> None: + cases = self._generate_mixed_cases() + results = select_testcases(filters=None, init_cases=cases, target_os=Ubuntu) + names = sorted(r.name for r in results) + self.assertEqual(names, ["any_linux", "ubuntu_only"]) + + def test_target_mariner_keeps_mariner_and_unrelated(self) -> None: + cases = self._generate_mixed_cases() + results = select_testcases(filters=None, init_cases=cases, target_os=CBLMariner) + names = sorted(r.name for r in results) + self.assertEqual(names, ["any_linux", "mariner_only", "not_ubuntu"]) + + +class GlobalRegistryPrefilterTestCase(TestCase): + """Integration test that exercises the same code path the runner uses, + via the global suite/case registry. Mirrors the existing pattern in + selftests/test_testselector.py (cleanup_cases_metadata in setUp + + generate_cases_metadata to register mock suites through the decorator + path). + """ + + def setUp(self) -> None: + # Avoid late import cycles by importing the existing fixture only + # when this test runs. + from selftests.test_testsuite import cleanup_cases_metadata + + cleanup_cases_metadata() + + def tearDown(self) -> None: + from selftests.test_testsuite import cleanup_cases_metadata + + cleanup_cases_metadata() + + def test_target_os_drops_from_global_registry(self) -> None: + from selftests.test_testsuite import generate_cases_metadata + + # Register the standard mock suites in the global registry. None of + # these mock cases declare a supported_os, so only the default + # ``unsupported_os=[Windows]`` applies. A Windows target must drop + # them all; an Ubuntu target must keep them all. + generate_cases_metadata() + + ubuntu_results = select_testcases(filters=None, target_os=Ubuntu) + windows_results = select_testcases(filters=None, target_os=Windows) + + self.assertGreater( + len(ubuntu_results), + 0, + "Ubuntu target should keep mock cases that have no OS restriction", + ) + self.assertEqual( + len(windows_results), + 0, + "Windows target should drop mock cases (default unsupported_os=[Windows])", + ) From 41523294da0df60abed3a1f8945e4a87eff26567 Mon Sep 17 00:00:00 2001 From: Johnson George Date: Wed, 20 May 2026 18:50:36 -0700 Subject: [PATCH 2/2] narrow supported_os metadata to match before_case runtime guards Align test case/suite metadata with the runtime isinstance + SkippedException guards so the distro pre-filter can drop inapplicable cases before VM deployment. --- lisa/microsoft/testsuites/azure/net/component.py | 2 ++ lisa/microsoft/testsuites/cdrom/cdrom.py | 5 +++-- .../testsuites/vm_extensions/azure_disk_encryption.py | 1 + .../testsuites/vm_extensions/azure_keyvault_vm_extension.py | 5 +++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lisa/microsoft/testsuites/azure/net/component.py b/lisa/microsoft/testsuites/azure/net/component.py index 395222ee23..280c4605a9 100644 --- a/lisa/microsoft/testsuites/azure/net/component.py +++ b/lisa/microsoft/testsuites/azure/net/component.py @@ -11,6 +11,7 @@ TestCaseMetadata, TestSuite, TestSuiteMetadata, + simple_requirement, ) from lisa.operating_system import CBLMariner from lisa.tools import Conntrack, Ipset, Iptables, Ping @@ -25,6 +26,7 @@ such as conntrack, ipset, and iptables. It ensures that connection tracking, IP-based filtering, and custom rule management operate as expected """, + requirement=simple_requirement(supported_os=[CBLMariner]), ) class NetworkComponentTest(TestSuite): def before_case(self, log: Logger, **kwargs: Any) -> None: diff --git a/lisa/microsoft/testsuites/cdrom/cdrom.py b/lisa/microsoft/testsuites/cdrom/cdrom.py index c1d9c5da5d..7a82bdd697 100644 --- a/lisa/microsoft/testsuites/cdrom/cdrom.py +++ b/lisa/microsoft/testsuites/cdrom/cdrom.py @@ -6,7 +6,7 @@ from assertpy import assert_that from lisa import Node, SkippedException, TestCaseMetadata, TestSuite, TestSuiteMetadata -from lisa.operating_system import CBLMariner, Debian, Linux, Ubuntu +from lisa.operating_system import CBLMariner, Debian, Ubuntu from lisa.sut_orchestrator import AZURE, HYPERV from lisa.testsuite import simple_requirement from lisa.tools import Gcc @@ -34,7 +34,8 @@ class CdromSuite(TestSuite): """, priority=2, requirement=simple_requirement( - supported_os=[Linux], supported_platform_type=[AZURE, HYPERV] + supported_os=[Debian, CBLMariner], + supported_platform_type=[AZURE, HYPERV], ), ) def verify_cdrom_device_status_code(self, node: Node) -> None: diff --git a/lisa/microsoft/testsuites/vm_extensions/azure_disk_encryption.py b/lisa/microsoft/testsuites/vm_extensions/azure_disk_encryption.py index 342d809b25..4222c793fc 100644 --- a/lisa/microsoft/testsuites/vm_extensions/azure_disk_encryption.py +++ b/lisa/microsoft/testsuites/vm_extensions/azure_disk_encryption.py @@ -145,6 +145,7 @@ def verify_azure_disk_encryption_enabled( priority=1, requirement=simple_requirement( min_memory_mb=MIN_REQUIRED_MEMORY_MB, + supported_os=[Redhat, CentOs, Oracle, Ubuntu, CBLMariner], supported_features=[AzureExtension, CvmDisabled()], supported_platform_type=[AZURE], ), diff --git a/lisa/microsoft/testsuites/vm_extensions/azure_keyvault_vm_extension.py b/lisa/microsoft/testsuites/vm_extensions/azure_keyvault_vm_extension.py index bb1b68cdcb..02a6916c4d 100644 --- a/lisa/microsoft/testsuites/vm_extensions/azure_keyvault_vm_extension.py +++ b/lisa/microsoft/testsuites/vm_extensions/azure_keyvault_vm_extension.py @@ -19,7 +19,7 @@ simple_requirement, ) from lisa.base_tools.service import Service -from lisa.operating_system import BSD, CBLMariner, Ubuntu +from lisa.operating_system import CBLMariner, Ubuntu from lisa.sut_orchestrator.azure.common import ( add_system_assign_identity, assign_access_policy, @@ -97,7 +97,8 @@ def before_case(self, log: Logger, **kwargs: Any) -> None: """, priority=1, requirement=simple_requirement( - supported_features=[AzureExtension], unsupported_os=[BSD] + supported_os=[Ubuntu, CBLMariner], + supported_features=[AzureExtension], ), ) def verify_key_vault_extension(