From c01d35adcbfdf2323153a08aac10a991a67c1f3c Mon Sep 17 00:00:00 2001 From: Zack Cerza Date: Tue, 2 Jun 2026 17:13:22 -0600 Subject: [PATCH 1/7] Host.os_type(): Return "darwin" on macOS Signed-off-by: Zack Cerza --- ceph_devstack/host.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ceph_devstack/host.py b/ceph_devstack/host.py index c5005e0d..f2668d53 100644 --- a/ceph_devstack/host.py +++ b/ceph_devstack/host.py @@ -74,10 +74,15 @@ def kernel_version(self) -> Version: def os_type(self) -> str: if not hasattr(self, "_os_type"): - proc = self.run(["bash", "-c", ". /etc/os-release && echo $ID"]) - assert proc.stdout is not None - assert proc.wait() == 0, "is /etc/os-release missing?" - self._os_type = proc.stdout.read().decode().strip().lower() + proc = self.run(["uname"]) + assert proc.wait() == 0, "uname doesn't work?!" + if (uname_str := proc.stdout.read().decode().strip().lower()) == "linux": + proc = self.run(["bash", "-c", ". /etc/os-release && echo $ID"]) + assert proc.stdout is not None + assert proc.wait() == 0, "is /etc/os-release missing?" + self._os_type = proc.stdout.read().decode().strip().lower() + else: + self._os_type = uname_str return self._os_type async def podman_info(self, force: bool = False) -> Dict: From 0228981388669078fe64cb55c2bd7b89f51860ba Mon Sep 17 00:00:00 2001 From: Zack Cerza Date: Wed, 3 Jun 2026 15:59:07 -0600 Subject: [PATCH 2/7] Host: Add podman_machine_info() Signed-off-by: Zack Cerza --- ceph_devstack/host.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ceph_devstack/host.py b/ceph_devstack/host.py index f2668d53..bb876750 100644 --- a/ceph_devstack/host.py +++ b/ceph_devstack/host.py @@ -1,4 +1,5 @@ import asyncio +import json import logging import os import pathlib @@ -94,6 +95,13 @@ async def podman_info(self, force: bool = False) -> Dict: self._podman_info = yaml.safe_load(stdout.decode().strip()) return self._podman_info + async def podman_machine_info(self) -> List[Dict]: + proc = await self.arun(["podman", "machine", "list", "--format", "json"]) + assert proc.stdout is not None + await proc.wait() + stdout = await proc.stdout.read() + return json.loads(stdout) + async def selinux_enforcing(self) -> bool: proc = await host.arun(["cat", "/sys/fs/selinux/enforce"]) assert proc.stdout is not None From ed869d01c955fb4bb59fb0b9ac58332d5343d294 Mon Sep 17 00:00:00 2001 From: Zack Cerza Date: Thu, 4 Jun 2026 10:54:25 -0600 Subject: [PATCH 3/7] Host: Add package_manager() Signed-off-by: Zack Cerza --- ceph_devstack/host.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ceph_devstack/host.py b/ceph_devstack/host.py index bb876750..85c88114 100644 --- a/ceph_devstack/host.py +++ b/ceph_devstack/host.py @@ -86,6 +86,15 @@ def os_type(self) -> str: self._os_type = uname_str return self._os_type + def package_manager(self) -> str | None: + if self.os_type in ["centos", "rhel", "alma", "rocky", "fedora"]: + return "dnf" + elif self.os_type in ["debian", "ubuntu"]: + return "apt" + elif self.os_type == "darwin": + return "brew" + raise RuntimeError("Can't determine package manager") + async def podman_info(self, force: bool = False) -> Dict: if force or not hasattr(self, "_podman_info"): proc = await self.arun(["podman", "info"]) From e94427f54ce0d2a39985bd7dec506db1c6bcfd0e Mon Sep 17 00:00:00 2001 From: Zack Cerza Date: Wed, 3 Jun 2026 17:53:55 -0600 Subject: [PATCH 4/7] TestCheckRequirements: Fix behavior on darwin We weren't mocking enough. Signed-off-by: Zack Cerza --- tests/test_requirements_core.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_requirements_core.py b/tests/test_requirements_core.py index 1d268fb6..b28b531f 100644 --- a/tests/test_requirements_core.py +++ b/tests/test_requirements_core.py @@ -283,15 +283,15 @@ async def test_check_requirements_returns_false_on_overlay_failure(self): with ( patch("ceph_devstack.requirements.PodmanPlatform") as MockPlatform, patch("ceph_devstack.requirements.PodmanGraphDriver") as MockGraph, + patch("ceph_devstack.requirements.PodmanVersion") as MockVersion, + patch("ceph_devstack.requirements.KernelVersionForOverlay") as MockKernel, + patch("ceph_devstack.requirements.CgroupV2") as MockCGroup, ): - mock_platform = AsyncMock() - mock_platform.evaluate = AsyncMock(return_value=True) - MockPlatform.return_value = mock_platform - - mock_graph = AsyncMock() - mock_graph.evaluate = AsyncMock(return_value=False) - MockGraph.return_value = mock_graph - + MockPlatform.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockGraph.return_value = AsyncMock(evaluate=AsyncMock(return_value=False)) + MockVersion.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockKernel.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockCGroup.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) result = await requirements.check_requirements() assert result is False From 87bafb0206cfe71823091e92edb236d178371818 Mon Sep 17 00:00:00 2001 From: Zack Cerza Date: Tue, 2 Jun 2026 17:13:49 -0600 Subject: [PATCH 5/7] requirements: Improve doctor for macOS, Ubuntu We'll now check whether the podman VM exists and is started, separately, and implement fixes for both. Signed-off-by: Zack Cerza --- ceph_devstack/requirements.py | 100 +++++++++++++++++++++----------- tests/test_requirements_core.py | 88 ++++++++++++++++++++++++++-- 2 files changed, 149 insertions(+), 39 deletions(-) diff --git a/ceph_devstack/requirements.py b/ceph_devstack/requirements.py index 2d71ba0e..32d49b42 100644 --- a/ceph_devstack/requirements.py +++ b/ceph_devstack/requirements.py @@ -1,5 +1,4 @@ import shlex -import sys from pathlib import Path from packaging.version import parse as parse_version, Version @@ -40,7 +39,7 @@ async def suggest(self): async def fix(self) -> bool: assert self.fix_cmd, "Attempted to fix without a fix command" - proc = await self.host.arun(self.fix_cmd) + proc = await self.host.arun(self.fix_cmd, stream_output=True) return await proc.wait() == 0 @@ -48,36 +47,49 @@ class LocalRequirement(Requirement): host = local_host -class PodmanPlatform(Requirement): +class LocalFixableRequirement(FixableRequirement): + host = local_host + + +class PodmanPlatform(LocalFixableRequirement): + suggest_msg = "podman not found" + + @property + def fix_cmd(self): + host_os = self.host.os_type() + if host_os == "darwin": + return ["brew", "install", "podman"] + return ["sudo", host.package_manager(), "install", "-y", "podman"] + async def check(self): - result = False try: - podman_info = await self.host.podman_info() + await self.host.podman_info() + return True except FileNotFoundError: logger.error("podman not found. Try: dnf install podman") return False - try: - host_os = ( - podman_info["host"].get("Os") or podman_info["host"]["os"] - ).lower() - if host_os == "linux": - result = True - except KeyError: - host_os = sys.platform.lower() - result = False - if sys.platform == "darwin": - logger.error( - "The podman machine (VM) is not running. " - "Try: podman machine init --now" - ) - else: - logger.error( - "Unknown error trying to query podman. Is podman installed?" - ) - return result - if host_os != "linux": - logger.error("The platform '{host_os}' is not currently supported.") - return result + + +class PodmanMachinePresent(FixableRequirement): + suggest_msg = "podman machine (VM) not present" + fix_cmd = ["podman", "machine", "init", "--now"] + + async def check(self): + machine_infos = await host.podman_machine_info() + if machine_infos and (machine_info := machine_infos[-1]): + return machine_info.get("Created") is not None + return False + + +class PodmanMachineRunning(LocalFixableRequirement): + suggest_msg = "podman machine (VM) not running" + fix_cmd = ["podman", "machine", "start"] + + async def check(self): + machine_infos = await host.podman_machine_info() + if machine_infos and (machine_info := machine_infos[-1]): + return machine_info.get("Running", False) + return False class PodmanGraphDriver(Requirement): @@ -150,6 +162,12 @@ async def check(self): class PodmanRuntime(Requirement): + @property + def fix_cmd(self): + if self.host.os_type() != "darwin": + return ["sudo", self.host.package_manager(), "install", "-y", "crun"] + return [] + async def check(self): podman_info = await self.host.podman_info() storage_conf_path = podman_info["store"]["configFile"] @@ -194,22 +212,31 @@ async def check(self): class PodmanDNSPlugin(FixableRequirement): suggest_msg = "Could not find the podman DNS plugin" - def __init__(self): + @property + def dns_plugin_path(self): + os_type = self.host.os_type() + if os_type in ["ubuntu", "debian"]: + return "/usr/lib/cni/dnsname" + return "/usr/libexec/cni/dnsname" + + @property + def check_cmd(self): + return ["test", "-x", self.dns_plugin_path] + + @property + def fix_cmd(self): os_type = self.host.os_type() if os_type == "centos": - dns_plugin_path = "/usr/libexec/cni/dnsname" - self.check_cmd = ["test", "-x", dns_plugin_path] - self.fix_cmd = ["sudo", "dnf", "install", "-y", dns_plugin_path] + return ["sudo", "dnf", "install", "-y", self.dns_plugin_path] elif os_type in ["ubuntu", "debian"]: - dns_plugin_path = "/usr/lib/cni/dnsname" - self.check_cmd = ["test", "-x", dns_plugin_path] - self.fix_cmd = [ + return [ "sudo", "apt", "install", "-y", "golang-github-containernetworking-plugin-dnsname", ] + return [] class FuseOverlayfsPresence(FixableRequirement): @@ -234,6 +261,11 @@ class AppArmorProfile(FixableRequirement): async def check_requirements(): if not await PodmanPlatform().evaluate(): return False + if local_host.os_type() == "darwin": + if not await PodmanMachinePresent().evaluate(): + return False + if not await PodmanMachineRunning().evaluate(): + return False result = True # kernel and podman versions for native overlay filesystem diff --git a/tests/test_requirements_core.py b/tests/test_requirements_core.py index b28b531f..faec36f3 100644 --- a/tests/test_requirements_core.py +++ b/tests/test_requirements_core.py @@ -1,7 +1,7 @@ import asyncio import pytest from packaging.version import parse as parse_version -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from ceph_devstack import config, requirements @@ -17,6 +17,11 @@ def req(cls): return cls() +@pytest.fixture(scope="class", params=["centos", "ubuntu", "debian"]) +def os_type(request): + return request.param + + class TestRequirement: @pytest.fixture(scope="class") def cls(self): @@ -113,6 +118,73 @@ def test_local_requirement_uses_local_host(self, req): assert req.host == requirements.local_host +class TestPodmanPlatform: + @pytest.fixture(scope="class") + def cls(self): + return requirements.PodmanPlatform + + async def test_podman_present(self, cls): + with ( + patch( + "ceph_devstack.requirements.PodmanPlatform.host.podman_info" + ) as MockPodmanInfo, + patch("ceph_devstack.requirements.local_host") as MockLocalHost, + ): + MockLocalHost.os_type = MagicMock(return_value="darwin") + MockPodmanInfo.return_value = {} + req = cls() + assert await req.check() is True + + async def test_podman_missing(self, cls): + with ( + patch( + "ceph_devstack.requirements.PodmanPlatform.host.podman_info" + ) as MockPodmanInfo, + patch("ceph_devstack.requirements.local_host") as MockLocalHost, + ): + MockLocalHost.os_type = MagicMock(return_value="darwin") + MockPodmanInfo.side_effect = FileNotFoundError + req = cls() + assert await req.check() is False + + +class TestPodmanMachinePresent: + @pytest.fixture(scope="class") + def cls(self): + return requirements.PodmanMachinePresent + + @pytest.mark.parametrize( + "info,success", [[{}, False], [{"Created": "some_timestamp"}, True]] + ) + async def test_podman_machine_present(self, cls, info, success): + with patch("ceph_devstack.requirements.host", AsyncMock()) as MockHost: + MockHost.podman_machine_info = AsyncMock(return_value=[info]) + req = cls() + assert await req.check() is success + + +class TestPodmanMachineRunning: + @pytest.fixture(scope="class") + def cls(self): + return requirements.PodmanMachineRunning + + @pytest.mark.parametrize( + "info,success", + [[{}, False], [{"Running": False}, False], [{"Running": True}, True]], + ) + async def test_podman_machine_running(self, cls, info, success): + with patch("ceph_devstack.requirements.host", AsyncMock()) as MockHost: + MockHost.podman_machine_info = AsyncMock(return_value=[info]) + req = cls() + assert await req.check() is success + + +class TestPodmanRuntime: + @pytest.fixture(scope="class") + def cls(self): + return requirements.PodmanRuntime + + class TestPodmanVersionInit: @pytest.fixture(scope="class") def cls(self): @@ -223,10 +295,6 @@ class TestPodmanDNSPluginInit: def cls(self): return requirements.PodmanDNSPlugin - @pytest.fixture(scope="class", params=["centos", "ubuntu", "debian"]) - def os_type(self, request): - return request.param - @pytest.fixture(scope="class") def dns_plugin_path(self, os_type): if os_type == "centos": @@ -281,12 +349,14 @@ async def test_check_requirements_returns_false_when_podman_not_platform(self): async def test_check_requirements_returns_false_on_overlay_failure(self): with ( + patch("ceph_devstack.requirements.local_host") as MockLocalHost, patch("ceph_devstack.requirements.PodmanPlatform") as MockPlatform, patch("ceph_devstack.requirements.PodmanGraphDriver") as MockGraph, patch("ceph_devstack.requirements.PodmanVersion") as MockVersion, patch("ceph_devstack.requirements.KernelVersionForOverlay") as MockKernel, patch("ceph_devstack.requirements.CgroupV2") as MockCGroup, ): + MockLocalHost.os_type = MagicMock(return_value="centos") MockPlatform.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) MockGraph.return_value = AsyncMock(evaluate=AsyncMock(return_value=False)) MockVersion.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) @@ -297,6 +367,7 @@ async def test_check_requirements_returns_false_on_overlay_failure(self): async def test_check_requirements_returns_true_when_all_pass(self): with ( + patch("ceph_devstack.requirements.local_host") as MockLocalHost, patch("ceph_devstack.requirements.PodmanPlatform") as MockPlatform, patch("ceph_devstack.requirements.PodmanGraphDriver") as MockGraph, patch("ceph_devstack.requirements.PodmanVersion") as MockVersion, @@ -309,6 +380,7 @@ async def test_check_requirements_returns_true_when_all_pass(self): patch("ceph_devstack.requirements.host.selinux_enforcing") as mock_selinux, patch("ceph_devstack.requirements.SysctlValue") as MockSysctl, ): + MockLocalHost.os_type = MagicMock(return_value="centos") mock_platform = AsyncMock() mock_platform.evaluate = AsyncMock(return_value=True) MockPlatform.return_value = mock_platform @@ -348,6 +420,7 @@ async def test_check_requirements_returns_true_when_all_pass(self): async def test_check_requirements_returns_false_on_runtime_failure(self): with ( + patch("ceph_devstack.requirements.local_host") as MockLocalHost, patch("ceph_devstack.requirements.PodmanPlatform") as MockPlatform, patch("ceph_devstack.requirements.PodmanGraphDriver") as MockGraph, patch("ceph_devstack.requirements.PodmanVersion") as MockVersion, @@ -359,6 +432,7 @@ async def test_check_requirements_returns_false_on_runtime_failure(self): patch("ceph_devstack.requirements.PodmanRuntime") as MockRuntime, patch("ceph_devstack.requirements.host.selinux_enforcing") as mock_selinux, ): + MockLocalHost.os_type = MagicMock(return_value="centos") mock_platform = AsyncMock() mock_platform.evaluate = AsyncMock(return_value=True) MockPlatform.return_value = mock_platform @@ -394,6 +468,7 @@ async def test_check_requirements_returns_false_on_runtime_failure(self): async def test_check_requirements_returns_false_on_selinux_bool_failure(self): with ( + patch("ceph_devstack.requirements.local_host") as MockLocalHost, patch("ceph_devstack.requirements.PodmanPlatform") as MockPlatform, patch("ceph_devstack.requirements.PodmanGraphDriver") as MockGraph, patch("ceph_devstack.requirements.KernelVersionForOverlay") as MockKernel, @@ -405,6 +480,7 @@ async def test_check_requirements_returns_false_on_selinux_bool_failure(self): patch("ceph_devstack.requirements.host.selinux_enforcing") as mock_selinux, patch("ceph_devstack.requirements.SELinuxBoolean") as MockSELinuxBoolean, ): + MockLocalHost.os_type = MagicMock(return_value="centos") mock_platform = AsyncMock() mock_platform.evaluate = AsyncMock(return_value=True) MockPlatform.return_value = mock_platform @@ -445,6 +521,7 @@ async def test_check_requirements_returns_false_on_selinux_bool_failure(self): async def test_check_requirements_returns_false_on_sysctl_failure(self): with ( + patch("ceph_devstack.requirements.local_host") as MockLocalHost, patch("ceph_devstack.requirements.PodmanPlatform") as MockPlatform, patch("ceph_devstack.requirements.PodmanGraphDriver") as MockGraph, patch("ceph_devstack.requirements.PodmanVersion") as MockVersion, @@ -457,6 +534,7 @@ async def test_check_requirements_returns_false_on_sysctl_failure(self): patch("ceph_devstack.requirements.host.selinux_enforcing") as mock_selinux, patch("ceph_devstack.requirements.SysctlValue") as MockSysctl, ): + MockLocalHost.os_type = MagicMock(return_value="centos") mock_platform = AsyncMock() mock_platform.evaluate = AsyncMock(return_value=True) MockPlatform.return_value = mock_platform From 4ccaa4095fd7a4c7fcacc6273e7032366af3fbdf Mon Sep 17 00:00:00 2001 From: Zack Cerza Date: Wed, 3 Jun 2026 19:24:59 -0600 Subject: [PATCH 6/7] tests: Reduce some test repetition Signed-off-by: Zack Cerza --- .../resources/ceph/test_requirements_ceph.py | 28 +-- tests/test_requirements_core.py | 175 +++++------------- 2 files changed, 54 insertions(+), 149 deletions(-) diff --git a/tests/resources/ceph/test_requirements_ceph.py b/tests/resources/ceph/test_requirements_ceph.py index c4ccd74f..cc4b58a7 100644 --- a/tests/resources/ceph/test_requirements_ceph.py +++ b/tests/resources/ceph/test_requirements_ceph.py @@ -216,15 +216,11 @@ async def test_check_requirements_returns_true_when_all_pass(self): ) as MockLoopCtrlWrite, patch("ceph_devstack.host.host.selinux_enforcing") as mock_selinux, ): - mock_has_sudo = AsyncMock() - mock_has_sudo.evaluate = AsyncMock(return_value=True) - MockHasSudo.return_value = mock_has_sudo - mock_loop_ctrl = AsyncMock() - mock_loop_ctrl.evaluate = AsyncMock(return_value=True) - MockLoopCtrl.return_value = mock_loop_ctrl - mock_loop_ctrl_write = AsyncMock() - mock_loop_ctrl_write.evaluate = AsyncMock(return_value=True) - MockLoopCtrlWrite.return_value = mock_loop_ctrl_write + MockHasSudo.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockLoopCtrl.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockLoopCtrlWrite.return_value = AsyncMock( + evaluate=AsyncMock(return_value=True) + ) mock_selinux.return_value = False result = await devstack.check_requirements() assert result is True @@ -247,15 +243,11 @@ async def test_check_requirements_returns_false_when_repo_missing(self): patch("ceph_devstack.host.host.selinux_enforcing") as mock_selinux, patch("ceph_devstack.host.host.path_exists") as mock_path_exists, ): - mock_has_sudo = AsyncMock() - mock_has_sudo.evaluate = AsyncMock(return_value=True) - MockHasSudo.return_value = mock_has_sudo - mock_loop_ctrl = AsyncMock() - mock_loop_ctrl.evaluate = AsyncMock(return_value=True) - MockLoopCtrl.return_value = mock_loop_ctrl - mock_loop_ctrl_write = AsyncMock() - mock_loop_ctrl_write.evaluate = AsyncMock(return_value=True) - MockLoopCtrlWrite.return_value = mock_loop_ctrl_write + MockHasSudo.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockLoopCtrl.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockLoopCtrlWrite.return_value = AsyncMock( + evaluate=AsyncMock(return_value=True) + ) mock_selinux.return_value = False mock_path_exists.return_value = False result = await devstack.check_requirements() diff --git a/tests/test_requirements_core.py b/tests/test_requirements_core.py index faec36f3..3a43261b 100644 --- a/tests/test_requirements_core.py +++ b/tests/test_requirements_core.py @@ -354,14 +354,14 @@ async def test_check_requirements_returns_false_on_overlay_failure(self): patch("ceph_devstack.requirements.PodmanGraphDriver") as MockGraph, patch("ceph_devstack.requirements.PodmanVersion") as MockVersion, patch("ceph_devstack.requirements.KernelVersionForOverlay") as MockKernel, - patch("ceph_devstack.requirements.CgroupV2") as MockCGroup, + patch("ceph_devstack.requirements.CgroupV2") as MockCgroup, ): MockLocalHost.os_type = MagicMock(return_value="centos") MockPlatform.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) MockGraph.return_value = AsyncMock(evaluate=AsyncMock(return_value=False)) MockVersion.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) MockKernel.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) - MockCGroup.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockCgroup.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) result = await requirements.check_requirements() assert result is False @@ -381,40 +381,17 @@ async def test_check_requirements_returns_true_when_all_pass(self): patch("ceph_devstack.requirements.SysctlValue") as MockSysctl, ): MockLocalHost.os_type = MagicMock(return_value="centos") - mock_platform = AsyncMock() - mock_platform.evaluate = AsyncMock(return_value=True) - MockPlatform.return_value = mock_platform - - mock_graph = AsyncMock() - mock_graph.evaluate = AsyncMock(return_value=True) - MockGraph.return_value = mock_graph - - mock_version = AsyncMock() - mock_version.evaluate = AsyncMock(return_value=True) - MockVersion.return_value = mock_version - - mock_kernel = AsyncMock() - mock_kernel.evaluate = AsyncMock(return_value=True) - MockKernel.return_value = mock_kernel - - mock_cgroup = AsyncMock() - mock_cgroup.evaluate = AsyncMock(return_value=True) - MockCgroup.return_value = mock_cgroup - - mock_kernel_cgroup = AsyncMock() - mock_kernel_cgroup.evaluate = AsyncMock(return_value=True) - MockKernelCgroup.return_value = mock_kernel_cgroup - - mock_runtime = AsyncMock() - mock_runtime.evaluate = AsyncMock(return_value=True) - MockRuntime.return_value = mock_runtime - + MockPlatform.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockGraph.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockVersion.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockKernel.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockCgroup.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockKernelCgroup.return_value = AsyncMock( + evaluate=AsyncMock(return_value=True) + ) + MockRuntime.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) mock_selinux.return_value = False - - mock_sysctl = AsyncMock() - mock_sysctl.evaluate = AsyncMock(return_value=True) - MockSysctl.return_value = mock_sysctl - + MockSysctl.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) result = await requirements.check_requirements() assert result is True @@ -433,36 +410,16 @@ async def test_check_requirements_returns_false_on_runtime_failure(self): patch("ceph_devstack.requirements.host.selinux_enforcing") as mock_selinux, ): MockLocalHost.os_type = MagicMock(return_value="centos") - mock_platform = AsyncMock() - mock_platform.evaluate = AsyncMock(return_value=True) - MockPlatform.return_value = mock_platform - - mock_graph = AsyncMock() - mock_graph.evaluate = AsyncMock(return_value=True) - MockGraph.return_value = mock_graph - - mock_version = AsyncMock() - mock_version.evaluate = AsyncMock(return_value=True) - MockVersion.return_value = mock_version - - mock_kernel = AsyncMock() - mock_kernel.evaluate = AsyncMock(return_value=True) - MockKernel.return_value = mock_kernel - - mock_cgroup = AsyncMock() - mock_cgroup.evaluate = AsyncMock(return_value=True) - MockCgroup.return_value = mock_cgroup - + MockPlatform.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockGraph.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockVersion.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockKernel.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockCgroup.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) mock_kernel_cgroup = AsyncMock() mock_kernel_cgroup.evaluate = AsyncMock(return_value=True) MockKernelCgroup.return_value = mock_kernel_cgroup - - mock_runtime = AsyncMock() - mock_runtime.evaluate = AsyncMock(return_value=False) - MockRuntime.return_value = mock_runtime - + MockRuntime.return_value = AsyncMock(evaluate=AsyncMock(return_value=False)) mock_selinux.return_value = False - result = await requirements.check_requirements() assert result is False @@ -471,6 +428,7 @@ async def test_check_requirements_returns_false_on_selinux_bool_failure(self): patch("ceph_devstack.requirements.local_host") as MockLocalHost, patch("ceph_devstack.requirements.PodmanPlatform") as MockPlatform, patch("ceph_devstack.requirements.PodmanGraphDriver") as MockGraph, + patch("ceph_devstack.requirements.PodmanVersion") as MockVersion, patch("ceph_devstack.requirements.KernelVersionForOverlay") as MockKernel, patch("ceph_devstack.requirements.CgroupV2") as MockCgroup, patch( @@ -481,43 +439,21 @@ async def test_check_requirements_returns_false_on_selinux_bool_failure(self): patch("ceph_devstack.requirements.SELinuxBoolean") as MockSELinuxBoolean, ): MockLocalHost.os_type = MagicMock(return_value="centos") - mock_platform = AsyncMock() - mock_platform.evaluate = AsyncMock(return_value=True) - MockPlatform.return_value = mock_platform - - mock_graph = AsyncMock() - mock_graph.evaluate = AsyncMock(return_value=True) - MockGraph.return_value = mock_graph - - with patch("ceph_devstack.requirements.PodmanVersion") as MockVersion: - mock_version = AsyncMock() - mock_version.evaluate = AsyncMock(return_value=True) - MockVersion.return_value = mock_version - - mock_kernel = AsyncMock() - mock_kernel.evaluate = AsyncMock(return_value=True) - MockKernel.return_value = mock_kernel - - mock_cgroup = AsyncMock() - mock_cgroup.evaluate = AsyncMock(return_value=True) - MockCgroup.return_value = mock_cgroup - - mock_kernel_cgroup = AsyncMock() - mock_kernel_cgroup.evaluate = AsyncMock(return_value=True) - MockKernelCgroup.return_value = mock_kernel_cgroup - - mock_runtime = AsyncMock() - mock_runtime.evaluate = AsyncMock(return_value=True) - MockRuntime.return_value = mock_runtime - - mock_selinux.return_value = True - - mock_sel = AsyncMock() - mock_sel.evaluate = AsyncMock(return_value=False) - MockSELinuxBoolean.return_value = mock_sel - - result = await requirements.check_requirements() - assert result is False + MockPlatform.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockGraph.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockVersion.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockKernel.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockCgroup.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockKernelCgroup.return_value = AsyncMock( + evaluate=AsyncMock(return_value=True) + ) + MockRuntime.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + mock_selinux.return_value = True + MockSELinuxBoolean.return_value = AsyncMock( + evaluate=AsyncMock(return_value=False) + ) + result = await requirements.check_requirements() + assert result is False async def test_check_requirements_returns_false_on_sysctl_failure(self): with ( @@ -535,39 +471,16 @@ async def test_check_requirements_returns_false_on_sysctl_failure(self): patch("ceph_devstack.requirements.SysctlValue") as MockSysctl, ): MockLocalHost.os_type = MagicMock(return_value="centos") - mock_platform = AsyncMock() - mock_platform.evaluate = AsyncMock(return_value=True) - MockPlatform.return_value = mock_platform - - mock_graph = AsyncMock() - mock_graph.evaluate = AsyncMock(return_value=True) - MockGraph.return_value = mock_graph - - mock_version = AsyncMock() - mock_version.evaluate = AsyncMock(return_value=True) - MockVersion.return_value = mock_version - - mock_kernel = AsyncMock() - mock_kernel.evaluate = AsyncMock(return_value=True) - MockKernel.return_value = mock_kernel - - mock_cgroup = AsyncMock() - mock_cgroup.evaluate = AsyncMock(return_value=True) - MockCgroup.return_value = mock_cgroup - - mock_kernel_cgroup = AsyncMock() - mock_kernel_cgroup.evaluate = AsyncMock(return_value=True) - MockKernelCgroup.return_value = mock_kernel_cgroup - - mock_runtime = AsyncMock() - mock_runtime.evaluate = AsyncMock(return_value=True) - MockRuntime.return_value = mock_runtime - + MockPlatform.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockGraph.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockVersion.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockKernel.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockCgroup.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) + MockKernelCgroup.return_value = AsyncMock( + evaluate=AsyncMock(return_value=True) + ) + MockRuntime.return_value = AsyncMock(evaluate=AsyncMock(return_value=True)) mock_selinux.return_value = False - - mock_sysctl = AsyncMock() - mock_sysctl.evaluate = AsyncMock(return_value=False) - MockSysctl.return_value = mock_sysctl - + MockSysctl.return_value = AsyncMock(evaluate=AsyncMock(return_value=False)) result = await requirements.check_requirements() assert result is False From 8ccded14a3d310cfaaf8f3760334edcf42f8d89c Mon Sep 17 00:00:00 2001 From: Zack Cerza Date: Thu, 4 Jun 2026 10:45:37 -0600 Subject: [PATCH 7/7] .github: Move all workflows to uv Signed-off-by: Zack Cerza --- .github/workflows/ci.yml | 22 ++++++++++------------ .github/workflows/tox.yml | 6 +----- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbdbb47c..ad9b2c32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,22 +30,20 @@ jobs: - uses: actions/checkout@v2 - name: Install packages run: sudo apt-get update && sudo apt-get install podman golang-github-containernetworking-plugin-dnsname sqlite3 jq - - name: Create virtualenv - run: python3 -m venv venv - - name: Install - run: ./venv/bin/pip3 install -e . + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Set owner for /dev/loop-control run: sudo chown $(whoami) /dev/loop-control - name: Configure - run: ./venv/bin/ceph-devstack config set containers.postgres.count 0 + run: uv run ceph-devstack config set containers.postgres.count 0 - name: Doctor - run: ./venv/bin/ceph-devstack -v doctor --fix + run: uv run ceph-devstack -v doctor --fix - name: Build - run: ./venv/bin/ceph-devstack -v build + run: uv run ceph-devstack -v build - name: Create - run: ./venv/bin/ceph-devstack -v create + run: uv run ceph-devstack -v create - name: Start - run: ./venv/bin/ceph-devstack -v start + run: uv run ceph-devstack -v start - name: Check Status run: podman ps -a - name: Check ssh access to testnode container @@ -54,7 +52,7 @@ jobs: if: failure() run: podman exec testnode_0 journalctl - name: Wait - run: ./venv/bin/ceph-devstack wait teuthology + run: uv run ceph-devstack wait teuthology - name: Dump logs if: success() || failure() run: podman logs -f teuthology @@ -84,6 +82,6 @@ jobs: name: archive path: /tmp/artifacts/archive.tar - name: Stop - run: ./venv/bin/ceph-devstack -v stop + run: uv run ceph-devstack -v stop - name: Remove - run: ./venv/bin/ceph-devstack -v remove + run: uv run ceph-devstack -v remove diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index c3cadab3..b51ab96a 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -27,11 +27,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - - name: Install pipx - run: pip install pipx - name: Install uv - run: pipx install uv - - name: Run pipx ensurepath - run: pipx ensurepath + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Run unit tests run: uv run --extra test tox