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>
168 lines
7.3 KiB
Python
168 lines
7.3 KiB
Python
"""Unit tests for generate_inventory().
|
|
|
|
Uses SimpleNamespace duck-typed objects so no DB or SQLAlchemy is required.
|
|
"""
|
|
from types import SimpleNamespace
|
|
|
|
import yaml
|
|
|
|
from steward.ansible.inventory_gen import generate_inventory, inventory_to_yaml
|
|
|
|
|
|
def _group(name, ansible_vars=None):
|
|
g = SimpleNamespace()
|
|
g.name = name
|
|
g.ansible_vars = ansible_vars or {}
|
|
return g
|
|
|
|
|
|
def _target(name, address, ansible_vars=None, groups=None):
|
|
t = SimpleNamespace()
|
|
t.name = name
|
|
t.address = address
|
|
t.ansible_vars = ansible_vars or {}
|
|
t.groups = groups or []
|
|
return t
|
|
|
|
|
|
def test_empty_targets():
|
|
result = generate_inventory([])
|
|
assert result == {"all": {"hosts": []}, "_meta": {"hostvars": {}}}
|
|
|
|
|
|
def test_single_target_no_groups():
|
|
target = _target("webserver", "192.168.1.10", {"ansible_user": "ubuntu"})
|
|
result = generate_inventory([target])
|
|
assert result["all"]["hosts"] == ["webserver"]
|
|
assert result["_meta"]["hostvars"]["webserver"]["ansible_host"] == "192.168.1.10"
|
|
assert result["_meta"]["hostvars"]["webserver"]["ansible_user"] == "ubuntu"
|
|
assert set(result.keys()) == {"all", "_meta"}
|
|
|
|
|
|
def test_ansible_host_overrides_group_var():
|
|
"""ansible_host is always target.address, even if a group sets it to something else."""
|
|
grp = _group("servers", {"ansible_host": "10.0.0.1"})
|
|
target = _target("host1", "192.168.1.50", groups=[grp])
|
|
result = generate_inventory([target])
|
|
assert result["_meta"]["hostvars"]["host1"]["ansible_host"] == "192.168.1.50"
|
|
|
|
|
|
def test_group_vars_applied():
|
|
grp = _group("webservers", {"http_port": 80, "ansible_user": "deploy"})
|
|
target = _target("web1", "192.168.1.10", groups=[grp])
|
|
result = generate_inventory([target])
|
|
hostvars = result["_meta"]["hostvars"]["web1"]
|
|
assert hostvars["http_port"] == 80
|
|
assert hostvars["ansible_user"] == "deploy"
|
|
assert result["webservers"]["hosts"] == ["web1"]
|
|
assert result["webservers"]["vars"]["http_port"] == 80
|
|
|
|
|
|
def test_host_vars_override_group_vars():
|
|
"""Per-target ansible_vars beat group vars for the same key."""
|
|
grp = _group("g1", {"ansible_user": "group_user"})
|
|
target = _target("t1", "1.2.3.4", {"ansible_user": "host_user"}, groups=[grp])
|
|
result = generate_inventory([target])
|
|
assert result["_meta"]["hostvars"]["t1"]["ansible_user"] == "host_user"
|
|
|
|
|
|
def test_group_var_precedence_is_alphabetical():
|
|
"""When multiple groups set the same key, alphabetically-last group wins (Ansible default)."""
|
|
g_a = _group("aaa", {"timeout": 10})
|
|
g_b = _group("bbb", {"timeout": 20})
|
|
target = _target("t1", "1.1.1.1", groups=[g_b, g_a]) # deliberately reversed order
|
|
result = generate_inventory([target])
|
|
# Groups sorted alphabetically: aaa then bbb → bbb wins
|
|
assert result["_meta"]["hostvars"]["t1"]["timeout"] == 20
|
|
|
|
|
|
def test_multiple_targets_in_group():
|
|
grp = _group("db", {"db_port": 5432})
|
|
t1 = _target("db1", "10.0.0.1", groups=[grp])
|
|
t2 = _target("db2", "10.0.0.2", groups=[grp])
|
|
result = generate_inventory([t1, t2])
|
|
assert set(result["db"]["hosts"]) == {"db1", "db2"}
|
|
assert result["all"]["hosts"] == ["db1", "db2"]
|
|
|
|
|
|
def test_target_in_multiple_groups():
|
|
g1 = _group("web", {"role": "web"})
|
|
g2 = _group("prod", {"env": "production"})
|
|
target = _target("web-prod-01", "192.168.1.1", groups=[g1, g2])
|
|
result = generate_inventory([target])
|
|
assert "web-prod-01" in result["web"]["hosts"]
|
|
assert "web-prod-01" in result["prod"]["hosts"]
|
|
assert result["_meta"]["hostvars"]["web-prod-01"]["role"] == "web"
|
|
assert result["_meta"]["hostvars"]["web-prod-01"]["env"] == "production"
|
|
|
|
|
|
# ── inventory_to_yaml: STATIC inventory (hosts is a dict, not a list) ──────────
|
|
|
|
def test_yaml_hosts_is_dict_with_hostvars():
|
|
target = _target("web1", "192.168.1.10", {"ansible_user": "ubuntu"})
|
|
parsed = yaml.safe_load(inventory_to_yaml(generate_inventory([target])))
|
|
# The bug was a LIST here; a static YAML inventory requires a dict keyed by host.
|
|
assert isinstance(parsed["all"]["hosts"], dict)
|
|
assert parsed["all"]["hosts"]["web1"]["ansible_host"] == "192.168.1.10"
|
|
assert parsed["all"]["hosts"]["web1"]["ansible_user"] == "ubuntu"
|
|
|
|
|
|
def test_yaml_groups_under_children():
|
|
grp = _group("db", {"db_port": 5432})
|
|
t1 = _target("db1", "10.0.0.1", groups=[grp])
|
|
t2 = _target("db2", "10.0.0.2", groups=[grp])
|
|
parsed = yaml.safe_load(inventory_to_yaml(generate_inventory([t1, t2])))
|
|
assert set(parsed["all"]["hosts"]) == {"db1", "db2"}
|
|
assert set(parsed["all"]["children"]["db"]["hosts"]) == {"db1", "db2"}
|
|
assert parsed["all"]["children"]["db"]["vars"]["db_port"] == 5432
|
|
|
|
|
|
def test_yaml_injected_hostvars_survive():
|
|
"""Extra per-host vars injected after generate_inventory (e.g. steward_token)."""
|
|
inv = generate_inventory([_target("h1", "1.2.3.4")])
|
|
inv["_meta"]["hostvars"]["h1"]["steward_token"] = "tok with space"
|
|
parsed = yaml.safe_load(inventory_to_yaml(inv))
|
|
assert parsed["all"]["hosts"]["h1"]["steward_token"] == "tok with space"
|
|
|
|
|
|
def test_yaml_empty_targets():
|
|
parsed = yaml.safe_load(inventory_to_yaml(generate_inventory([])))
|
|
assert parsed == {"all": {"hosts": {}}}
|
|
|
|
|
|
def test_describe_scope_names_what_a_run_targeted():
|
|
from steward.ansible.inventory_gen import describe_scope
|
|
targets, groups = {"t1": "web-1"}, {"g1": "Docker Swarm"}
|
|
assert describe_scope("steward:all", targets, groups) == "all inventory targets"
|
|
assert describe_scope("steward:target:t1", targets, groups) == "target web-1"
|
|
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"}
|