You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #2192 (foundation) and PR #2193 (pytest + Zuul infrastructure). Part of Tier 4 (#2199). osism/utils/rabbitmq.py (173 LOC) resolves RabbitMQ node IPs by combining ansible-inventory calls, ansible-facts cached in Redis, and Jinja2 template resolution from internal_interface. Includes a small helper for loading the encrypted RabbitMQ password.
Scope
Add tests/unit/utils/test_rabbitmq.py covering both functions in osism/utils/rabbitmq.py.
Test targets
get_rabbitmq_node_addresses() — rabbitmq.py:13
Patch:
osism.utils.rabbitmq.subprocess.check_output
osism.utils.rabbitmq.get_inventory_path
osism.utils.rabbitmq.get_hosts_from_inventory
osism.utils.rabbitmq.utils.redis (the lazy attribute on osism.utils) — easiest via mocker.patch("osism.utils.rabbitmq.utils.redis", ...) to a MagicMock
Inventory & host discovery
Inventory call returns valid JSON, two hosts → both processed in alphabetical order (the function calls .sort() on the host list)
get_hosts_from_inventory returns [] → returns None, error logged "No hosts found in rabbitmq group"
redis.get("ansible_facts<host>") returns None → host skipped, error logged, processing continues to next host
Hostvars query returns no internal_interface → host skipped, error logged
internal_interface is a literal string ("eth0") → used directly without templating
internal_interface is a Jinja template ("{{ ansible_local.testbed_network_devices.management }}") → resolved against the facts dict via dotted path traversal
Template path resolves to a non-string (None, dict, int) → host skipped, error logged
Template path traversal hits a non-dict mid-walk (e.g. ansible_local.foo is a string) → resolves to None, host skipped
Interface name normalization: "eth0.100" → "ansible_eth0_100", "eth-0" → "ansible_eth_0"
Interface facts present but no ipv4 key → host skipped, error logged
ipv4 present but address empty/missing → host skipped, error logged
Happy path: returns [("10.0.0.5", "host1"), ("10.0.0.6", "host2")] in alphabetical-host order
Aggregate results
All hosts skipped → returns None, error logged "Could not retrieve address for any RabbitMQ node"
At least one address → returns the list (no error)
load_rabbitmq_password() — rabbitmq.py:139
Patch os.path.exists and osism.tasks.conductor.utils.load_yaml_file (or import path used by the production code: from osism.tasks.conductor.utils import load_yaml_file — patch at the call site).
Background
Follow-up to #2192 (foundation) and PR #2193 (pytest + Zuul infrastructure). Part of Tier 4 (#2199).
osism/utils/rabbitmq.py(173 LOC) resolves RabbitMQ node IPs by combiningansible-inventorycalls, ansible-facts cached in Redis, and Jinja2 template resolution frominternal_interface. Includes a small helper for loading the encrypted RabbitMQ password.Scope
Add
tests/unit/utils/test_rabbitmq.pycovering both functions inosism/utils/rabbitmq.py.Test targets
get_rabbitmq_node_addresses()—rabbitmq.py:13Patch:
osism.utils.rabbitmq.subprocess.check_outputosism.utils.rabbitmq.get_inventory_pathosism.utils.rabbitmq.get_hosts_from_inventoryosism.utils.rabbitmq.utils.redis(the lazy attribute onosism.utils) — easiest viamocker.patch("osism.utils.rabbitmq.utils.redis", ...)to aMagicMockInventory & host discovery
.sort()on the host list)get_hosts_from_inventoryreturns[]→ returnsNone, error logged "No hosts found in rabbitmq group"subprocess.check_output(first call, group listing) raisesCalledProcessError→ returnsNone, error loggedNone(json.JSONDecodeError), error loggedNone, error loggedPer-host resolution
redis.get("ansible_facts<host>")returnsNone→ host skipped, error logged, processing continues to next hostinternal_interface→ host skipped, error loggedinternal_interfaceis a literal string ("eth0") → used directly without templatinginternal_interfaceis a Jinja template ("{{ ansible_local.testbed_network_devices.management }}") → resolved against the facts dict via dotted path traversalNone, dict, int) → host skipped, error loggedansible_local.foois a string) → resolves toNone, host skipped"eth0.100"→"ansible_eth0_100","eth-0"→"ansible_eth_0"ipv4key → host skipped, error loggedipv4present butaddressempty/missing → host skipped, error logged[("10.0.0.5", "host1"), ("10.0.0.6", "host2")]in alphabetical-host orderAggregate results
None, error logged "Could not retrieve address for any RabbitMQ node"load_rabbitmq_password()—rabbitmq.py:139Patch
os.path.existsandosism.tasks.conductor.utils.load_yaml_file(or import path used by the production code:from osism.tasks.conductor.utils import load_yaml_file— patch at the call site).None, error loggedload_yaml_filereturnsNone→ returnsNone, error logged "Empty or invalid secrets file"load_yaml_filereturns a non-dict (e.g. a list) → returnsNone, error loggedrabbitmq_passwordkey → returnsNone, error loggedrabbitmq_password=" hunter2 "→ returns"hunter2"(stripped)rabbitmq_password=42(int) → coerced viastr(...).strip()→"42"load_yaml_fileraises → returnsNone, error loggedMocking hints
subprocess.check_outputcalls per host (one for the group, one for the host vars). Useside_effectwith a list of payloads:redis.getis aMagicMock; set itsside_effectper host or use.return_value=json.dumps(facts).encode().get_inventory_pathreturns a string path; the function is covered in Unit tests for osism/utils/inventory.py #2194, so just stub it here.RABBITMQ_USERis a module constant — verify its value ("openstack") in one quick test.Definition of Done
tests/unit/utils/test_rabbitmq.pycreatedpytest --cov=osism.utils.rabbitmqshows ≥ 95 %pipenv run pytest tests/unit/utils/test_rabbitmq.pypasses locallyflake8,mypy,python-blackremain greenpython-osism-unit-testspassesDependencies