Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
6 changes: 1 addition & 5 deletions .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 26 additions & 4 deletions ceph_devstack/host.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import json
import logging
import os
import pathlib
Expand Down Expand Up @@ -74,12 +75,26 @@ 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

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"])
Expand All @@ -89,6 +104,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
Expand Down
100 changes: 66 additions & 34 deletions ceph_devstack/requirements.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import shlex
import sys

from pathlib import Path
from packaging.version import parse as parse_version, Version
Expand Down Expand Up @@ -40,44 +39,57 @@ 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


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):
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
28 changes: 10 additions & 18 deletions tests/resources/ceph/test_requirements_ceph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Loading
Loading