From 9470c0eb757b8fc794e213bf79176b84d808d8a0 Mon Sep 17 00:00:00 2001 From: Michael Sherman Date: Mon, 26 Oct 2026 18:24:45 +0000 Subject: [PATCH] Run container execute and download over exec sessions - execute returned partial output and no exit code for a command that ran longer than 5 s. - download failed after 60 s, and extracted archives without checking where files land. Use chameleon-zunclient's execute_run and copy_from, from 2025.1.2. A downloaded directory now lands in dest//, as upload puts it, instead of its contents landing directly in dest. Co-Authored-By: Claude Opus 5.5 --- chi/container.py | 13 +++++-------- requirements.txt | 2 +- tests/test_container.py | 42 +++++++++++++++++++---------------------- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/chi/container.py b/chi/container.py index 636b346..9a34be4 100644 --- a/chi/container.py +++ b/chi/container.py @@ -560,7 +560,9 @@ def execute(container_ref: "str", command: "str") -> "dict": Returns: A summary of the output of the command, with "output" and "exit_code". """ - return zun().containers.execute(container_ref, command=command, run=True) + result = zun().containers.execute_run(container_ref, command) + output = (result.stdout + result.stderr).decode(errors="replace") + return {"output": output, "exit_code": result.returncode} def upload(container_ref: "str", source: "str", dest: "str") -> "dict": @@ -592,19 +594,14 @@ def download(container_ref: "str", source: "str", dest: "str"): Download a file or directory from a running container. - This method requires your running container to include - both the POSIX sh and GNU tar utilities. + This method requires your running container to include tar. Args: container_ref (str): The name or ID of the container. source (str): The (container) path of the file or directory. dest (str): The (local) path to download to. """ - res = zun().containers.get_archive(container_ref, source) - fd = io.BytesIO(res["data"]) - with tarfile.open(fileobj=fd, mode="r") as tar: - tar.extraction_filter = lambda member, path: member - tar.extractall(dest) + zun().containers.copy_from(container_ref, source, dest) def wait_for_active(container_ref: "str", timeout: int = (60 * 2)) -> "Container": diff --git a/requirements.txt b/requirements.txt index 60fc42d..41c4234 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-manilaclient python-neutronclient python-novaclient python-swiftclient -chameleon-zunclient>=2025.1.1 +chameleon-zunclient>=2025.1.2 ipython ipydatagrid ipywidgets diff --git a/tests/test_container.py b/tests/test_container.py index adf1fcd..8691b21 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -20,7 +20,7 @@ import pytest from zunclient.exceptions import Conflict -from chi.container import Container, download, upload +from chi.container import Container, download, execute, upload @pytest.fixture() @@ -88,29 +88,25 @@ def test_upload_creates_tar_and_calls_put_archive(mocker): assert os.path.basename(tmpfile.name) in names -def test_download_extracts_tar_and_writes_file(mocker): - # Patch zun client +def test_execute_uses_execute_run(mocker): + zun_mock = mocker.patch("chi.container.zun")() + result = zun_mock.containers.execute_run.return_value + result.stdout = b"out\n" + result.stderr = b"err\n" + result.returncode = 3 + + assert execute("container_id", "ls -l") == {"output": "out\nerr\n", "exit_code": 3} + zun_mock.containers.execute_run.assert_called_once_with("container_id", "ls -l") + + +def test_download_uses_copy_from(mocker): zun_mock = mocker.patch("chi.container.zun")() - # Create a tar archive in memory with a test file - file_content = b"test content" - file_name = "testfile.txt" - tar_bytes_io = io.BytesIO() - with tarfile.open(fileobj=tar_bytes_io, mode="w") as tar: - info = tarfile.TarInfo(name=file_name) - info.size = len(file_content) - tar.addfile(info, io.BytesIO(file_content)) - tar_bytes = tar_bytes_io.getvalue() - zun_mock.containers.get_archive.return_value = {"data": tar_bytes} - - # Use a temporary directory for extraction - with tempfile.TemporaryDirectory() as tmpdir: - dest_path = os.path.join(tmpdir, file_name) - - download("container_id", file_name, tmpdir) - # Check that the file was extracted - assert os.path.exists(dest_path) - with open(dest_path, "rb") as f: - assert f.read() == file_content + + download("container_id", "/root/data", "/tmp/dest") + + zun_mock.containers.copy_from.assert_called_once_with( + "container_id", "/root/data", "/tmp/dest" + ) def test_delete_calls_force(mocker):