mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
Whether a service gets a GPU needs two facts to agree: the declaration in
applications under services.<service>.gpu, and sys-svc-container's stat of
/dev/nvidia0 on the target host. The conjunction was restated at each call
site, and web-svc-libretranslate restated only the first half: it appended the
-cuda image suffix from the declaration alone. That tag has no arm64 manifest,
so the build died with `no match for platform in manifest`, which 10 of the 79
failing jobs of run 37252453141 carry, surfacing through
sys-svc-compose/handlers/{swarm,compose}.yml.
lookup('gpu', application_id[, service]) is now the single place that answers
it. Three call sites collapse onto it: both libretranslate roles' *_GPU var
and the runtime line of sys-svc-container's resource template. A lookup runs
on the controller and cannot stat the target, so it reads the register out of
the play's variables; the applications view comes through lookup_loader
because test_no_cache_imports_in_lookups forbids a lookup the direct
utils.cache import.
The coercion is ansible's boolean(), not python's bool(). The call sites this
replaces read the flag through Jinja's `| bool`, which answers False for the
strings "false", "False", "0" and "no" where bool() answers True, and a
templated declaration renders to such a string. Measured over 13 values the
two disagree on four of them; boolean(strict=False) matches `| bool` on all
13. Two tests pin it, and with bool() restored exactly the "false" string test
fails.
Measured rather than argued: over the merged applications, 1688
(application, service) pairs resolve the predicate identically under both
coercions, 0 disagreements. The predicate turns on for 21 services, not 5,
because resource_filter falls back to services.<entity_name>.gpu and every
service of a role whose entity declares gpu inherits it.
The assert_gpu include in both roles dropped its two trailing conditions: they
re-checked the register that *_GPU now already contains.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
102 lines
3.5 KiB
Python
102 lines
3.5 KiB
Python
"""Unit tests for plugins/lookup/gpu.py.
|
|
|
|
Mocks the Ansible-side machinery (loader, lookup_loader, templar) and pins
|
|
that the lookup answers True only when the declaration AND the host fact
|
|
agree. The declaration resolution itself belongs to resource_filter and is
|
|
covered by tests.unit.python.plugins.filter.test_resource_filter.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
import unittest.mock as mock
|
|
from typing import ClassVar
|
|
|
|
from ansible.errors import AnsibleError
|
|
|
|
from plugins.lookup.gpu import LookupModule
|
|
|
|
APPLICATIONS: ClassVar[dict] = {
|
|
"web-svc-libretranslate": {"services": {"libretranslate": {"gpu": True}}},
|
|
"web-app-plain": {"services": {"plain": {"gpu": False}}},
|
|
"web-app-stringy": {"services": {"stringy": {"gpu": "false"}}},
|
|
"web-app-truthy": {"services": {"truthy": {"gpu": "yes"}}},
|
|
}
|
|
|
|
PRESENT: ClassVar[dict] = {"sys_svc_container_nvidia_device": {"stat": {"exists": True}}}
|
|
ABSENT: ClassVar[dict] = {"sys_svc_container_nvidia_device": {"stat": {"exists": False}}}
|
|
|
|
|
|
class _Templar:
|
|
def __init__(self, vars_=None):
|
|
self.available_variables = vars_ or {}
|
|
|
|
def template(self, value, **_):
|
|
return value
|
|
|
|
|
|
def _make_lookup(vars_=None):
|
|
lm = LookupModule()
|
|
lm._templar = _Templar(vars_ or {})
|
|
lm._loader = mock.MagicMock()
|
|
return lm
|
|
|
|
|
|
def _run(lm, terms):
|
|
loaded = mock.MagicMock()
|
|
loaded.run.return_value = [APPLICATIONS]
|
|
with mock.patch("plugins.lookup.gpu.lookup_loader") as loader:
|
|
loader.get.return_value = loaded
|
|
return lm.run(terms, variables=lm._templar.available_variables)
|
|
|
|
|
|
class TestGpuLookup(unittest.TestCase):
|
|
def test_declared_and_device_present_is_true(self):
|
|
lm = _make_lookup(PRESENT)
|
|
self.assertEqual(_run(lm, ["web-svc-libretranslate", "libretranslate"]), [True])
|
|
|
|
def test_declared_without_a_device_is_false(self):
|
|
"""The case that selected a -cuda tag with no manifest for the platform."""
|
|
lm = _make_lookup(ABSENT)
|
|
self.assertEqual(_run(lm, ["web-svc-libretranslate", "libretranslate"]), [False])
|
|
|
|
def test_undeclared_with_a_device_is_false(self):
|
|
lm = _make_lookup(PRESENT)
|
|
self.assertEqual(_run(lm, ["web-app-plain", "plain"]), [False])
|
|
|
|
def test_missing_register_is_false(self):
|
|
"""Before sys-svc-container runs the register is absent, not falsy."""
|
|
lm = _make_lookup({})
|
|
self.assertEqual(_run(lm, ["web-svc-libretranslate", "libretranslate"]), [False])
|
|
|
|
def test_non_mapping_register_is_false(self):
|
|
lm = _make_lookup({"sys_svc_container_nvidia_device": "skipped"})
|
|
self.assertEqual(_run(lm, ["web-svc-libretranslate", "libretranslate"]), [False])
|
|
|
|
def test_a_false_string_declaration_is_false(self):
|
|
"""Jinja's `| bool` reads "false" as False where bool() reads it as True.
|
|
|
|
The call sites this lookup replaces used `| bool`, so answering True
|
|
here would attach a GPU runtime nothing asked for.
|
|
"""
|
|
lm = _make_lookup(PRESENT)
|
|
self.assertEqual(_run(lm, ["web-app-stringy", "stringy"]), [False])
|
|
|
|
def test_a_yes_string_declaration_is_true(self):
|
|
lm = _make_lookup(PRESENT)
|
|
self.assertEqual(_run(lm, ["web-app-truthy", "truthy"]), [True])
|
|
|
|
def test_rejects_no_terms(self):
|
|
lm = _make_lookup(PRESENT)
|
|
with self.assertRaises(AnsibleError):
|
|
_run(lm, [])
|
|
|
|
def test_rejects_three_terms(self):
|
|
lm = _make_lookup(PRESENT)
|
|
with self.assertRaises(AnsibleError):
|
|
_run(lm, ["a", "b", "c"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|