diff --git a/avocado/core/plugin_interfaces.py b/avocado/core/plugin_interfaces.py index 01424149c2..2ea0958394 100644 --- a/avocado/core/plugin_interfaces.py +++ b/avocado/core/plugin_interfaces.py @@ -426,6 +426,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 537ffc1c6b..8ad1ba455f 100644 --- a/avocado/plugins/runner_nrunner.py +++ b/avocado/plugins/runner_nrunner.py @@ -289,6 +289,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 ) @@ -325,8 +335,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 13bc05b0e2..811ceb5fe3 100644 --- a/avocado/plugins/spawners/lxc.py +++ b/avocado/plugins/spawners/lxc.py @@ -105,6 +105,12 @@ class LXCSpawner(Spawner, SpawnerMixin): METHODS = [SpawnMethod.STANDALONE_EXECUTABLE] slots_cache = {} + def is_operational(self): + if not LXC_AVAILABLE: + LOG.error("LXC python bindings not available on the system") + return False + return True + @staticmethod def run_container_cmd(container, command): with LXCStreamsFile() as tmp_out, LXCStreamsFile() as tmp_err: @@ -209,11 +215,6 @@ async def spawn_task(self, runtime_task): arch = self.config.get("spawner.lxc.arch") create_hook = self.config.get("spawner.lxc.create_hook") - if not LXC_AVAILABLE: - msg = "LXC python bindings not available on the system" - runtime_task.status = msg - return False - container_id = runtime_task.spawner_handle container = lxc.Container(container_id) if not container.defined: diff --git a/avocado/plugins/spawners/podman.py b/avocado/plugins/spawners/podman.py index 3001bce207..386860389b 100644 --- a/avocado/plugins/spawners/podman.py +++ b/avocado/plugins/spawners/podman.py @@ -138,6 +138,20 @@ def _get_podman_version(self): f'output received "{out}" does not match expected output' ) + def is_operational(self): + try: + _ = self.podman_version + except PodmanException as ex: + LOG.error(ex) + return False + + if self.podman_version[0] >= 3: + return True + LOG.error( + f"The podman binary f{self.podman_bin} did not report a suitable version (>= 3.0)" + ) + return False + @property def podman_version(self): if self._podman_version == (None, None, None): diff --git a/avocado/plugins/spawners/process.py b/avocado/plugins/spawners/process.py index 7dc69004aa..0f3294c6b1 100644 --- a/avocado/plugins/spawners/process.py +++ b/avocado/plugins/spawners/process.py @@ -33,6 +33,9 @@ class ProcessSpawner(Spawner, SpawnerMixin): description = "Process based spawner" METHODS = [SpawnMethod.STANDALONE_EXECUTABLE] + def is_operational(self): + return True + @staticmethod def is_task_alive(runtime_task): if runtime_task.spawner_handle is None: diff --git a/optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py b/optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py index 89a72d8b10..83dcee81fc 100644 --- a/optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py +++ b/optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py @@ -68,6 +68,9 @@ class RemoteSpawner(Spawner, SpawnerMixin): METHODS = [SpawnMethod.STANDALONE_EXECUTABLE] slots_cache = {} + def is_operational(self): + return True + @staticmethod async def run_remote_cmd_async(session, command, timeout): loop = asyncio.get_event_loop() diff --git a/selftests/check.py b/selftests/check.py index 0e516511de..713212c252 100755 --- a/selftests/check.py +++ b/selftests/check.py @@ -29,7 +29,7 @@ "nrunner-requirement": 16, "unit": 667, "jobs": 11, - "functional-parallel": 297, + "functional-parallel": 298, "functional-serial": 4, "optional-plugins": 0, "optional-plugins-golang": 2, diff --git a/selftests/functional/plugin/spawners/podman.py b/selftests/functional/plugin/spawners/podman.py index d5e5becd16..ad2d2fd490 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)