Skip to content

Commit

Permalink
Podman: add synchronous version of utilities
Browse files Browse the repository at this point in the history
While the Podman spawner benefits from the async APIs because of its
requirement to also make async APIs available, most common uses of the
utilities APIs are not in an async context.

This introduces a Podman class that is not async.

I've researched some prior art to avoid duplicating code, but it seems
there's no magic and bulletproof way.

Reference: https://github.com/elastic/elastic-transport-python/blob/main/elastic_transport/_transport.py
Reference: https://github.com/elastic/elastic-transport-python/blob/main/elastic_transport/_async_transport.py
Signed-off-by: Cleber Rosa <[email protected]>
  • Loading branch information
clebergnu committed Mar 1, 2024
1 parent ca88839 commit c201be9
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 19 deletions.
158 changes: 141 additions & 17 deletions avocado/utils/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@

import json
import logging
from asyncio import create_subprocess_exec, subprocess
import subprocess
from asyncio import create_subprocess_exec
from asyncio import subprocess as asyncio_subprocess
from shutil import which

LOG = logging.getLogger(__name__)
Expand All @@ -32,7 +34,20 @@ class PodmanException(Exception):
pass


class AsyncPodman:
class _Podman:

PYTHON_VERSION_COMMAND = json.dumps(
[
"/usr/bin/env",
"python3",
"-c",
(
"import sys; print(sys.version_info.major, "
"sys.version_info.minor, sys.executable)"
),
]
)

def __init__(self, podman_bin=None):
path = which(podman_bin or "podman")
if not path:
Expand All @@ -41,6 +56,125 @@ def __init__(self, podman_bin=None):

self.podman_bin = path


class Podman(_Podman):
def execute(self, *args):
"""Execute a command and return the returncode, stdout and stderr.
:param args: Variable length argument list to be used as argument
during execution.
:rtype: tuple with returncode, stdout and stderr.
"""
try:
LOG.debug("Executing %s", args)

cmd = [self.podman_bin, *args]
proc = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, stderr = proc.communicate()
LOG.debug("Return code: %s", proc.returncode)
LOG.debug("Stdout: %s", stdout.decode("utf-8", "replace"))
LOG.debug("Stderr: %s", stderr.decode("utf-8", "replace"))
except (FileNotFoundError, PermissionError) as ex:
# Since this method is also used by other methods, let's
# log here as well.
msg = "Could not execute the command."
LOG.error("%s: %s", msg, str(ex))
raise PodmanException(msg) from ex

if proc.returncode != 0:
command_args = " ".join(args)
msg = f'Failure from command "{self.podman_bin} {command_args}": returned code "{proc.returncode}" stderr: "{stderr}"'
LOG.error(msg)
raise PodmanException(msg)

return proc.returncode, stdout, stderr

def copy_to_container(self, container_id, src, dst):
"""Copy artifacts from src to container:dst.
This method allows copying the contents of src to the dst. Files will
be copied from the local machine to the container. The "src" argument
can be a file or a directory.
:param str container_id: string with the container identification.
:param str src: what file or directory you are trying to copy.
:param str dst: the destination inside the container.
:rtype: tuple with returncode, stdout and stderr.
"""
try:
return self.execute("cp", src, f"{container_id}:{dst}")
except PodmanException as ex:
error = f"Failed copying data to container {container_id}"
LOG.error(error)
raise PodmanException(error) from ex

def get_python_version(self, image):
"""Return the current Python version installed in an image.
:param str image: Image name. i.e: 'fedora:33'.
:rtype: tuple with both: major, minor numbers and executable path.
"""
try:
_, stdout, _ = self.execute(
"run", "--rm", f"--entrypoint={self.PYTHON_VERSION_COMMAND}", image
)
except PodmanException as ex:
raise PodmanException("Failed getting Python version.") from ex

if stdout:
output = stdout.decode().strip().split()
return int(output[0]), int(output[1]), output[2]

def get_container_info(self, container_id):
"""Return all information about specific container.
:param container_id: identifier of container
:type container_id: str
:rtype: dict
"""
try:
_, stdout, _ = self.execute(
"ps", "--all", "--format=json", "--filter", f"id={container_id}"
)
except PodmanException as ex:
raise PodmanException(
f"Failed getting information about container:" f" {container_id}."
) from ex
containers = json.loads(stdout.decode())
for container in containers:
if container["Id"] == container_id:
return container
return {}

def start(self, container_id):
"""Starts a container and return the returncode, stdout and stderr.
:param str container_id: Container identification string to start.
:rtype: tuple with returncode, stdout and stderr.
"""
try:
return self.execute("start", container_id)
except PodmanException as ex:
raise PodmanException("Failed to start the container.") from ex

def stop(self, container_id):
"""Stops a container and return the returncode, stdout and stderr.
:param str container_id: Container identification string to stop.
:rtype: tuple with returncode, stdout and stderr.
"""
try:
return self.execute("stop", "-t=0", container_id)
except PodmanException as ex:
raise PodmanException("Failed to stop the container.") from ex


class AsyncPodman(_Podman):
async def execute(self, *args):
"""Execute a command and return the returncode, stdout and stderr.
Expand All @@ -52,7 +186,10 @@ async def execute(self, *args):
LOG.debug("Executing %s", args)

proc = await create_subprocess_exec(
self.podman_bin, *args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
self.podman_bin,
*args,
stdout=asyncio_subprocess.PIPE,
stderr=asyncio_subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
LOG.debug("Return code: %s", proc.returncode)
Expand Down Expand Up @@ -98,22 +235,9 @@ async def get_python_version(self, image):
:param str image: Image name. i.e: 'fedora:33'.
:rtype: tuple with both: major, minor numbers and executable path.
"""

entrypoint = json.dumps(
[
"/usr/bin/env",
"python3",
"-c",
(
"import sys; print(sys.version_info.major, "
"sys.version_info.minor, sys.executable)"
),
]
)

try:
_, stdout, _ = await self.execute(
"run", "--rm", f"--entrypoint={entrypoint}", image
"run", "--rm", f"--entrypoint={self.PYTHON_VERSION_COMMAND}", image
)
except PodmanException as ex:
raise PodmanException("Failed getting Python version.") from ex
Expand Down
2 changes: 1 addition & 1 deletion selftests/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"nrunner-requirement": 16,
"unit": 667,
"jobs": 11,
"functional-parallel": 300,
"functional-parallel": 302,
"functional-serial": 4,
"optional-plugins": 0,
"optional-plugins-golang": 2,
Expand Down
31 changes: 30 additions & 1 deletion selftests/functional/utils/podman.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
from avocado import Test
from avocado.utils.podman import AsyncPodman
from avocado.utils.podman import AsyncPodman, Podman


class PodmanTest(Test):
def test_python_version(self):
"""
:avocado: dependency={"type": "package", "name": "podman", "action": "check"}
:avocado: dependency={"type": "podman-image", "uri": "fedora:38"}
:avocado: tags=slow
"""
podman = Podman()
result = podman.get_python_version("fedora:38")
self.assertEqual(result, (3, 11, "/usr/bin/python3"))

def test_container_info(self):
"""
:avocado: dependency={"type": "package", "name": "podman", "action": "check"}
:avocado: dependency={"type": "podman-image", "uri": "fedora:38"}
:avocado: tags=slow
"""
podman = Podman()
_, stdout, _ = podman.execute("create", "fedora:38", "/bin/bash")
container_id = stdout.decode().strip()
result = podman.get_container_info(container_id)
self.assertEqual(result["Id"], container_id)

podman.execute("rm", container_id)

result = podman.get_container_info(container_id)
self.assertEqual(result, {})


class AsyncPodmanTest(Test):
Expand Down

0 comments on commit c201be9

Please sign in to comment.