fix(ansible): group names Ansible accepts, and interpreter discovery without the warning (#5334)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 57s
CI / unit (push) Successful in 1m24s
CI / publish (push) Successful in 1m26s

Every run warned twice because of Steward's own inventory:
- 'Invalid characters were found in group names': the group 'Docker
  Swarm' went into the inventory as typed. ansible_group_name() cleans it
  to docker_swarm (lower case, [a-z0-9_], never a leading digit); two
  names that clean alike get _2, _3 so neither folds into the other. The
  group page says what plays call it. The display name is unchanged.
- Python interpreter discovery: ansible_env sets
  ANSIBLE_PYTHON_INTERPRETER=auto_silent unless the environment already
  sets one; a target or group's ansible_python_interpreter still wins.

Runs are scoped by target set (steward:group:<id>), not by group name,
and --limit is free text, so neither path depends on the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 00:01:54 -04:00
co-authored by Claude Opus 5.5
parent 7c346354f2
commit c678379646
8 changed files with 80 additions and 7 deletions
+3 -1
View File
@@ -102,11 +102,13 @@ async def test_a_group_page_names_its_members_and_what_runs_on_it(app):
async with app.test_request_context("/ansible/inventory/groups/g1"):
session["user_role"] = "operator"
page = await render_template(
"ansible/inventory/group_detail.html", group=swarm, used=[NS(id="s1", name="Nightly prune")],
"ansible/inventory/group_detail.html", group=swarm, ansible_name="docker_swarm",
used=[NS(id="s1", name="Nightly prune")],
all_targets=[d2, util], member_ids={"t1"}, ansible_vars_yaml="")
main = page[page.index("<main"):]
assert '1 member:' in main and 'href="/ansible/inventory/targets/t1">VDNT-Docker02</a>' in main
assert 'scheduled by <a href="/ansible/schedules/s1">Nightly prune</a>' in main
assert "plays call it <code>docker_swarm</code>" in main # #5334
# Deleting a group a schedule runs on says so before it goes.
assert 'data-confirm="Delete group Docker Swarm? 1 schedule aimed at it would find nothing to run on."' in main
assert not hex_colours(main), hex_colours(main)
+27
View File
@@ -138,3 +138,30 @@ def test_describe_scope_names_what_a_run_targeted():
assert describe_scope("steward:group:g1", targets, groups) == "group Docker Swarm"
assert describe_scope("steward:target:abcdef0123", targets, groups) == "target abcdef01 (deleted)"
assert describe_scope("repo:homelab:hosts.yml", targets, groups) == "homelab/hosts.yml"
# ── Group names Ansible accepts (#5334) ───────────────────────────────────────
def test_a_group_name_is_cleaned_for_ansible():
from steward.ansible.inventory_gen import ansible_group_name
assert ansible_group_name("Docker Swarm") == "docker_swarm"
assert ansible_group_name("web-servers (prod)") == "web_servers_prod"
assert ansible_group_name("2nd floor") == "g_2nd_floor"
assert ansible_group_name("!!!") == "group"
assert ansible_group_name("already_fine") == "already_fine"
def test_the_inventory_uses_the_clean_names_and_keeps_clashing_groups_apart():
swarm = _group("Docker Swarm", {"role": "swarm"})
a, b = _group("Web servers", {"tier": "a"}), _group("web-servers", {"tier": "b"})
inv = generate_inventory([_target("d1", "10.0.0.1", groups=[swarm]),
_target("w1", "10.0.0.2", groups=[a]),
_target("w2", "10.0.0.3", groups=[b])])
assert "Docker Swarm" not in inv and inv["docker_swarm"]["hosts"] == ["d1"]
assert inv["docker_swarm"]["vars"] == {"role": "swarm"}
# "Web servers" sorts before "web-servers", so it keeps the plain name.
assert inv["web_servers"] == {"hosts": ["w1"], "vars": {"tier": "a"}}
assert inv["web_servers_2"] == {"hosts": ["w2"], "vars": {"tier": "b"}}
import yaml
children = yaml.safe_load(inventory_to_yaml(inv))["all"]["children"]
assert set(children) == {"docker_swarm", "web_servers", "web_servers_2"}
+2 -2
View File
@@ -109,7 +109,7 @@ def test_ansible_run_scope_column_exists():
def test_create_target_and_group_and_fetch():
"""Can create a target+group, assign membership, and fetch via fetch_scope_targets."""
from steward.models.ansible_inventory import AnsibleTarget, AnsibleGroup
from steward.ansible.inventory_gen import fetch_scope_targets, generate_inventory
from steward.ansible.inventory_gen import ansible_group_name, fetch_scope_targets, generate_inventory
app = _make_app()
target_id = str(uuid.uuid4())
@@ -157,7 +157,7 @@ def test_create_target_and_group_and_fetch():
targets = await fetch_scope_targets(db, f"steward:group:{group_id}")
inv = generate_inventory(targets)
assert target_name in inv["all"]["hosts"]
assert target_name in inv[group_name]["hosts"]
assert target_name in inv[ansible_group_name(group_name)]["hosts"] # test_group_… (#5334)
assert inv["_meta"]["hostvars"][target_name]["ansible_host"] == "10.0.99.1"
assert inv["_meta"]["hostvars"][target_name]["ansible_user"] == "ubuntu"
assert inv["_meta"]["hostvars"][target_name]["env"] == "test"
+4
View File
@@ -49,6 +49,10 @@ def test_ansible_env_host_key_checking_toggle():
assert ansible_env({"host_key_checking": True}, {})["ANSIBLE_HOST_KEY_CHECKING"] == "True"
assert ansible_env({"host_key_checking": False}, {})["ANSIBLE_HOST_KEY_CHECKING"] == "False"
assert ansible_env({}, {})["ANSIBLE_HOST_KEY_CHECKING"] == "False"
# Interpreter discovery stays, without its warning on every run (#5334);
# an interpreter set in the environment is left as it is.
assert ansible_env({}, {})["ANSIBLE_PYTHON_INTERPRETER"] == "auto_silent"
assert ansible_env({}, {"ANSIBLE_PYTHON_INTERPRETER": "/usr/bin/python3"})["ANSIBLE_PYTHON_INTERPRETER"] == "/usr/bin/python3"
assert ansible_env(None, {})["ANSIBLE_HOST_KEY_CHECKING"] == "False"