mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
Every stage looped over every role of its group and skipped the ones this host
does not deploy, one task at a time, plus an unconditional handler flush behind
each. Measured on a one-app deploy on 2026-09-23: 132 of 139 entries skipped,
278 task deltas, 41.8 seconds of nothing.
The lookup now takes the `groups` magic variable and drops the entries no host
in the inventory carries, so they never reach the loop.
Which roles get deployed does not change, and that is provable rather than
hoped for: the body deploys a role exactly when application_allowed holds,
which requires the app to be in that host's group_names; if it is, that host is
in groups[app], so groups[app] is non-empty and the lookup keeps the entry. The
filter is therefore strictly weaker than the condition it precedes and can drop
nothing the body would have run.
The filter deliberately reads `groups` and not `group_names`. 8aea5a9b35
measured that a per-host condition above the loop makes ansible insert the
included blocks in host order instead of loop order, after which
svc-storage-nfs-client mounted an export svc-storage-nfs-server had not
deployed yet and the play died on 'mount.nfs4: Connection refused'
(run 31645153977). `groups` is the same mapping on every host, so every host
still walks one identical list, and tasks/utils/setup/role.yml keeps its `when`
exactly where that commit put it. Two of the new cases pin this: a role only
one host carries still enters the loop, and a group_names passed to the lookup
changes nothing.
First written the other way, with group_names in the lookup and the `when`
removed. A single-host deploy cannot show that defect, and it came back green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""The stage loop skips roles no host carries, and gives every host one list.
|
|
|
|
Dropping what nobody deploys is free: 132 of 139 entries were skipped one task
|
|
at a time on a single-app deploy, 278 task deltas, 41.8 seconds measured on
|
|
2026-09-23.
|
|
|
|
Dropping what *this* host does not carry is not free, and these cases forbid
|
|
it. 8aea5a9b35 measured that a per-host condition above the loop makes ansible
|
|
insert blocks in host order rather than loop order, after which
|
|
svc-storage-nfs-client mounted an export svc-storage-nfs-server had not
|
|
deployed yet (run 31645153977). The list therefore has to come out identical
|
|
for every host, and per-host narrowing stays in the body's ``when``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from typing import ClassVar
|
|
from unittest import mock
|
|
|
|
from plugins.lookup.group_roles import LookupModule
|
|
|
|
ENTRIES = [
|
|
{"role": "web-app-alpha", "app": "alpha"},
|
|
{"role": "web-app-beta", "app": "beta"},
|
|
{"role": "web-app-gamma", "app": "gamma"},
|
|
]
|
|
|
|
|
|
def run(**kwargs):
|
|
with mock.patch(
|
|
"plugins.lookup.group_roles.ordered_roles", return_value=list(ENTRIES)
|
|
):
|
|
return LookupModule().run(["web-app"], **kwargs)[0]
|
|
|
|
|
|
class TestWithoutAnInventory(unittest.TestCase):
|
|
def test_no_groups_returns_the_whole_group(self) -> None:
|
|
self.assertEqual(run(), ENTRIES)
|
|
|
|
|
|
class TestDroppingWhatNobodyDeploys(unittest.TestCase):
|
|
def test_a_role_no_host_carries_is_dropped(self) -> None:
|
|
selected = run(inventory_groups={"alpha": ["h1"], "beta": []})
|
|
self.assertEqual([entry["app"] for entry in selected], ["alpha"])
|
|
|
|
def test_an_empty_inventory_deploys_nothing(self) -> None:
|
|
self.assertEqual(run(inventory_groups={}), [])
|
|
|
|
def test_run_after_order_survives_the_filter(self) -> None:
|
|
selected = run(inventory_groups={"gamma": ["h1"], "alpha": ["h1"]})
|
|
self.assertEqual(
|
|
[entry["app"] for entry in selected],
|
|
["alpha", "gamma"],
|
|
"run_after order is the contract; the inventory's key order is not",
|
|
)
|
|
|
|
|
|
class TestSameListForEveryHost(unittest.TestCase):
|
|
"""The property 8aea5a9b35 paid for; losing it inverts deploy order."""
|
|
|
|
GROUPS: ClassVar[dict] = {"alpha": ["h1"], "beta": ["h2"], "gamma": ["h1", "h2"]}
|
|
|
|
def test_a_role_only_one_host_carries_still_enters_the_loop(self) -> None:
|
|
selected = [entry["app"] for entry in run(inventory_groups=self.GROUPS)]
|
|
self.assertEqual(
|
|
selected,
|
|
["alpha", "beta", "gamma"],
|
|
"beta belongs to h2 only; dropping it for h1 would give the two "
|
|
"hosts different loops and let host order decide item order",
|
|
)
|
|
|
|
def test_the_lookup_reads_no_per_host_variable(self) -> None:
|
|
selected = run(inventory_groups=self.GROUPS, group_names=["alpha"])
|
|
self.assertEqual(
|
|
[entry["app"] for entry in selected],
|
|
["alpha", "beta", "gamma"],
|
|
"a host's own groups must not reach this lookup; narrowing to one "
|
|
"host belongs in the body's when",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|