diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..26d2af05 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + test: + name: CI on python${{ matrix.python }} via ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-24.04 + python: "3.12" + steps: + - uses: actions/checkout@v2 + - name: Install packages + run: sudo apt install podman golang-github-containernetworking-plugin-dnsname + - name: Create virtualenv + run: python3 -m venv venv + - name: Install + run: ./venv/bin/pip3 install -e . + - name: Set owner for /dev/loop-control + run: sudo chown $(whoami) /dev/loop-control + - name: Doctor + run: ./venv/bin/ceph-devstack -v doctor --fix + - name: Build + run: ./venv/bin/ceph-devstack -v build + - name: Create + run: ./venv/bin/ceph-devstack -v create + - name: Start + run: ./venv/bin/ceph-devstack -v start + - name: Wait + run: ./venv/bin/ceph-devstack wait teuthology + - name: Dump logs + if: success() || failure() + run: podman logs -f teuthology + - name: Stop + run: ./venv/bin/ceph-devstack -v stop + - name: Remove + run: ./venv/bin/ceph-devstack -v remove diff --git a/ceph_devstack/__init__.py b/ceph_devstack/__init__.py index e5d822b8..c2c88d66 100644 --- a/ceph_devstack/__init__.py +++ b/ceph_devstack/__init__.py @@ -96,6 +96,14 @@ def parse_args(args: List[str]) -> argparse.Namespace: subparsers.add_parser( "watch", help="Monitor the cluster, recreating containers as necessary" ) + parser_wait = subparsers.add_parser( + "wait", + help="Wait for the specified container to exit. Exit with its exit code.", + ) + parser_wait.add_argument( + "container", + help="The container to wait for", + ) subparsers.add_parser("show-conf", help="show the configuration") return parser.parse_args(args) diff --git a/ceph_devstack/cli.py b/ceph_devstack/cli.py index d73b7617..b3116f94 100644 --- a/ceph_devstack/cli.py +++ b/ceph_devstack/cli.py @@ -34,9 +34,13 @@ async def run(): sys.exit(1) if args.command == "doctor": return - await obj.apply(args.command) + elif args.command == "wait": + return await obj.wait(container_name=args.container) + else: + await obj.apply(args.command) + return 0 try: - asyncio.run(run()) + sys.exit(asyncio.run(run())) except KeyboardInterrupt: logger.debug("Exiting!") diff --git a/ceph_devstack/host.py b/ceph_devstack/host.py index 58ad4b2d..cf1cc641 100644 --- a/ceph_devstack/host.py +++ b/ceph_devstack/host.py @@ -71,6 +71,14 @@ def kernel_version(self) -> Version: self._kernel_version = parse_version(raw_version.split("-")[0]) return self._kernel_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() + return self._os_type + async def podman_info(self, force: bool = False) -> Dict: if force or not hasattr(self, "_podman_info"): proc = await self.arun(["podman", "info"]) diff --git a/ceph_devstack/requirements.py b/ceph_devstack/requirements.py index 9332515f..0fb01193 100644 --- a/ceph_devstack/requirements.py +++ b/ceph_devstack/requirements.py @@ -191,16 +191,30 @@ async def check(self): class PodmanDNSPlugin(FixableRequirement): - dns_plugin_path = "/usr/libexec/cni/dnsname" - check_cmd = ["test", "-x", dns_plugin_path] suggest_msg = "Could not find the podman DNS plugin" - fix_cmd = ["sudo", "dnf", "install", dns_plugin_path] + + def __init__(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] + elif os_type in ["ubuntu", "debian"]: + dns_plugin_path = "/usr/lib/cni/dnsname" + self.check_cmd = ["test", "-x", dns_plugin_path] + self.fix_cmd = [ + "sudo", + "apt", + "install", + "-y", + "golang-github-containernetworking-plugin-dnsname", + ] class FuseOverlayfsPresence(FixableRequirement): check_cmd = ["command", "-v", "fuse-overlayfs"] suggest_msg = "Could not find fuse-overlayfs" - fix_cmd = ["sudo", "dnf", "install", "fuse-overlayfs"] + fix_cmd = ["sudo", "dnf", "install", "-y", "fuse-overlayfs"] async def check_requirements(): diff --git a/ceph_devstack/resources/ceph/__init__.py b/ceph_devstack/resources/ceph/__init__.py index dbc59b70..95a8754e 100644 --- a/ceph_devstack/resources/ceph/__init__.py +++ b/ceph_devstack/resources/ceph/__init__.py @@ -220,3 +220,12 @@ async def watch(self): await container.start() except KeyboardInterrupt: break + + async def wait(self, container_name: str): + for kind in (await self.get_containers()).keys(): + for name in await self.get_container_names(kind): + container = kind(name=name) + if container.name == container_name: + return await container.wait() + logger.error(f"Could not find container {container_name}") + return 1 diff --git a/ceph_devstack/resources/ceph/requirements.py b/ceph_devstack/resources/ceph/requirements.py index 2ae3c373..00d79678 100644 --- a/ceph_devstack/resources/ceph/requirements.py +++ b/ceph_devstack/resources/ceph/requirements.py @@ -53,6 +53,7 @@ def __init__(self): "(sudo", "dnf", "install", + "-y", "policycoreutils-devel", "selinux-policy-devel", "&&", diff --git a/ceph_devstack/resources/container.py b/ceph_devstack/resources/container.py index 1ce90222..c0c6d9ef 100644 --- a/ceph_devstack/resources/container.py +++ b/ceph_devstack/resources/container.py @@ -25,6 +25,7 @@ class Container(PodmanResource): stop_cmd: List[str] = ["podman", "container", "stop", "{name}"] exists_cmd: List[str] = ["podman", "container", "inspect", "{name}"] pull_cmd: List[str] = ["podman", "pull", "{image}"] + wait_cmd: List[str] = ["podman", "wait", "{name}"] env_vars: Dict[str, Optional[str]] = {} def __init__(self, name: str = ""): @@ -153,3 +154,11 @@ async def is_running(self): if not result: return False return result[0]["State"]["Status"].lower() == "running" + + async def wait(self) -> Optional[int]: + proc = await self.cmd(self.format_cmd(self.wait_cmd)) + out, err = await proc.communicate() + if proc.returncode: + logger.error(f"Could not wait for {self.name}: {err.decode().strip()}") + return proc.returncode + return int(out.decode().strip())