diff --git a/avocado/core/plugin_interfaces.py b/avocado/core/plugin_interfaces.py index 5382122b62..86faea44ae 100644 --- a/avocado/core/plugin_interfaces.py +++ b/avocado/core/plugin_interfaces.py @@ -424,6 +424,16 @@ async def update_requirement_cache(runtime_task, result): :type result: `avocado.core.teststatus.STATUSES` """ + @abc.abstractmethod + def is_operational(self): + """Checks whether this spawner is operationally capable to perform. + + :result: whether or not this spawner is operational on this system, + that is, whether it has all its requirements set up and + should be ready to operate successfully. + :rtype: bool + """ + class DeploymentSpawner(Spawner): """Spawners that needs basic deployment are based on this class. diff --git a/avocado/core/spawners/mock.py b/avocado/core/spawners/mock.py index 7671e9c5ba..1fd4b72720 100644 --- a/avocado/core/spawners/mock.py +++ b/avocado/core/spawners/mock.py @@ -17,6 +17,9 @@ class MockSpawner(Spawner): def __init__(self): # pylint: disable=W0231 self._known_tasks = {} + def is_operational(self): + return True + def is_task_alive(self, runtime_task): # pylint: disable=W0221 alive = self._known_tasks.get(runtime_task, None) # task was never spawned diff --git a/avocado/plugins/runner_nrunner.py b/avocado/plugins/runner_nrunner.py index b0ad4d7778..f5fcb80d3b 100644 --- a/avocado/plugins/runner_nrunner.py +++ b/avocado/plugins/runner_nrunner.py @@ -267,6 +267,16 @@ def run_suite(self, job, test_suite): job.interrupted_reason = f"Suite {test_suite.name} is disabled." return summary + spawner_name = test_suite.config.get("run.spawner") + spawner = SpawnerDispatcher(test_suite.config, job)[spawner_name].obj + if not spawner.is_operational(): + suite_name = f" {test_suite.name}" if test_suite.name else "" + msg = f'Spawner "{spawner_name}" is not operational, aborting execution of suite{suite_name}. Please check the logs for more information.' + LOG_JOB.error(msg) + job.interrupted_reason = msg + summary.add("INTERRUPTED") + return summary + test_suite.tests, missing_requirements = check_runnables_runner_requirements( test_suite.tests ) @@ -303,8 +313,6 @@ def run_suite(self, job, test_suite): if rt.task.category == "test" ] self.tsm = TaskStateMachine(self.runtime_tasks, self.status_repo) - spawner_name = test_suite.config.get("run.spawner") - spawner = SpawnerDispatcher(test_suite.config, job)[spawner_name].obj max_running = min( test_suite.config.get("run.max_parallel_tasks"), len(self.runtime_tasks) ) diff --git a/avocado/plugins/spawners/lxc.py b/avocado/plugins/spawners/lxc.py index bea95f44f9..f05440b4d2 100644 --- a/avocado/plugins/spawners/lxc.py +++ b/avocado/plugins/spawners/lxc.py @@ -105,6 +105,9 @@ class LXCSpawner(Spawner, SpawnerMixin): METHODS = [SpawnMethod.STANDALONE_EXECUTABLE] slots_cache = {} + def is_operational(self): + return True + @staticmethod def run_container_cmd(container, command): with LXCStreamsFile() as tmp_out, LXCStreamsFile() as tmp_err: diff --git a/avocado/plugins/spawners/podman.py b/avocado/plugins/spawners/podman.py index a618d94030..6a5fe636f8 100644 --- a/avocado/plugins/spawners/podman.py +++ b/avocado/plugins/spawners/podman.py @@ -111,6 +111,34 @@ def __init__(self, config=None, job=None): # pylint: disable=W0231 SpawnerMixin.__init__(self, config, job) self.environment = f"podman:{self.config.get('spawner.podman.image')}" + def is_operational(self): + podman_bin = self.config.get("spawner.podman.bin") + cmd = [podman_bin, "version", "--format={{.Version}}"] + try: + process = subprocess.Popen( + cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + except FileNotFoundError: + LOG.error(f"The podman binary {podman_bin} could not be found") + return False + out, _ = process.communicate() + if process.returncode != 0: + LOG.error( + f"The execution of the podman binary {podman_bin} signaled failure (return code {process.returncode}" + ) + return False + if out: + major, _, _ = [int(v) for v in out.decode().split(".")] + if major >= 3: + return True + LOG.error( + f"The podman binary f{podman_bin} did not report a suitable version (>= 3.0)" + ) + return False + def is_task_alive(self, runtime_task): # pylint: disable=W0221 if runtime_task.spawner_handle is None: return False diff --git a/avocado/plugins/spawners/process.py b/avocado/plugins/spawners/process.py index 3f4cdd8bc1..82bd3e958e 100644 --- a/avocado/plugins/spawners/process.py +++ b/avocado/plugins/spawners/process.py @@ -17,6 +17,9 @@ class ProcessSpawner(Spawner, SpawnerMixin): description = "Process based spawner" METHODS = [SpawnMethod.STANDALONE_EXECUTABLE] + def is_operational(self): + return True + async def _collect_task(self, task_handle): await task_handle.wait() diff --git a/selftests/functional/plugin/spawners/podman.py b/selftests/functional/plugin/spawners/podman.py index 7f0cb6a42b..187f0189e7 100644 --- a/selftests/functional/plugin/spawners/podman.py +++ b/selftests/functional/plugin/spawners/podman.py @@ -2,6 +2,7 @@ import os from avocado import Test +from avocado.core import exit_codes from avocado.core.job import Job from avocado.utils import process, script from selftests.utils import AVOCADO, BASEDIR @@ -116,3 +117,19 @@ def test_asset_files(self): self.assertEqual(result.exit_status, 0) self.assertIn("use_data.sh: STARTED", result.stdout_text) self.assertIn("use_data.sh: PASS", result.stdout_text) + + +class OperationalTest(Test): + def test_not_operational(self): + fake_podman_bin = os.path.join(BASEDIR, "examples", "tests", "false") + result = process.run( + f"{AVOCADO} run " + f"--job-results-dir {self.workdir} " + f"--disable-sysinfo --spawner=podman " + f"--spawner-podman-bin={fake_podman_bin} " + f"--spawner-podman-image=fedora:36 -- " + f"examples/tests/true", + ignore_status=True, + ) + self.assertEqual(result.exit_status, exit_codes.AVOCADO_JOB_INTERRUPTED) + self.assertIn('Spawner "podman" is not operational', result.stderr_text)