Skip to content

Add unit tests for ironic.py pure helpers#2342

Open
berendt wants to merge 1 commit into
mainfrom
implement/issue-2226-ironic-helper-tests
Open

Add unit tests for ironic.py pure helpers#2342
berendt wants to merge 1 commit into
mainfrom
implement/issue-2226-ironic-helper-tests

Conversation

@berendt

@berendt berendt commented Jun 10, 2026

Copy link
Copy Markdown
Member

Closes #2226.

Adds tests/unit/tasks/conductor/test_ironic_helpers.py covering the pure
helpers and the _prepare_node_attributes builder in
osism/tasks/conductor/ironic.py — everything testable without the full
sync orchestration (the sync entry points stay in the companion sub-issue).

Change set

A single new test module (no production code touched), organized by helper:

  • _derive_as_from_hostname_yrzn — storage vs. non-storage type digit,
    < 5 parts → None, single-digit padding, already-two-digit values left
    alone.
  • _get_metalbox_primary_ip4_fallback — invalid YAML, non-list setting,
    non-dict element skipped, tag stripped + role="metalbox" added (asserted
    via captured filter kwargs), first/second metalbox IP selection, and the
    two no-IP / no-metalbox warning paths.
  • _get_metalbox_primary_ip4 — no OOB IP (fallback not called), subnet
    match, no-match → fallback, matching subnet but no primary_ip4 → early
    None (no fallback), match on the second interface, and IPv4/IPv6 mixing.
  • _render_templates — flat/nested dict, nested list, list of dicts,
    non-template and non-string values untouched, multiple vars, in-place
    mutation returning None.
  • _prepare_node_attributes — base/config-context/custom-field merging
    and order, driver pruning, template variables (board creds, OOB address,
    ironic_osism_* propagation), osism-ipa-type=yrzn001 kernel enrichment
    (frr params, metalbox, AS fallback, unknown type), skip_kernel_params,
    extra_kernel_params, driver_info persistence, extra serialization, and
    the returned tuple.
  • _prettify_for_display — JSON parsing in extra, non-JSON left
    untouched, deep copy with/without extra.

Mocking notes

  • Module-level collaborators (deep_decrypt, deep_merge, get_vault,
    get_device_oob_ip, SUPPORTED_IPA_TYPES, _derive_as_from_hostname_yrzn,
    _get_metalbox_primary_ip4) are patched at osism.tasks.conductor.ironic.*.
  • The NetBox client is patched via osism.utils.nb (with create=True), which
    is what the helpers' local from osism import utils resolves to — matching
    the existing test_netbox.py and sonic test fixtures. ironic.py has no
    module-level utils attribute (it imports it as osism_utils), so the
    literal osism.tasks.conductor.ironic.utils.nb path from the issue is not a
    valid patch target; osism.utils.nb is the established equivalent.
  • deep_decrypt / deep_merge are stubbed as no-ops; their real behavior is
    covered by the conductor.utils tests, so the merge cases assert call
    identity/order rather than merged output.

Deviation from the issue

The issue lists _derive_as_from_hostname_yrzn("comp-nw-22-3-7-1")
"4200143307". The implementation pads rack/server and produces
"4200140703" (matches the function's own docstring example
stor-nw-22-60-59-64200155960). The test encodes the verified value
"4200140703"; the same case (non-storage type 4, single-digit padding) is
still covered.

Verification

  • black --check, flake8 — pass on the new file.
  • pytest / coverage — left to the python-osism-unit-tests Zuul job.

AI-assisted: Claude Code

Cover the helpers in osism/tasks/conductor/ironic.py that can be
tested without the full sync orchestration (issue #2226):

- _derive_as_from_hostname_yrzn
- _get_metalbox_primary_ip4_fallback
- _get_metalbox_primary_ip4
- _render_templates
- _prepare_node_attributes
- _prettify_for_display

Collaborators are patched at their ironic.py import sites. The NetBox
client is replaced via osism.utils.nb, which the helpers' local
"from osism import utils" resolves to, matching the existing
test_netbox and sonic test conventions.

Note: the issue's example asserting
_derive_as_from_hostname_yrzn("comp-nw-22-3-7-1") == "4200143307"
does not match the implementation, which pads rack/server and yields
"4200140703" (consistent with the function's own docstring example).
The test encodes the verified value.

Assisted-by: Claude:anthropic-opus-4-8
Signed-off-by: Christian Berendt <berendt@osism.tech>
@berendt berendt force-pushed the implement/issue-2226-ironic-helper-tests branch from 75a2e60 to 1ce607c Compare June 10, 2026 20:49
@berendt berendt marked this pull request as ready for review June 10, 2026 20:55
@berendt berendt requested a review from ideaship June 10, 2026 20:55

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The local _YRZN_MAPPING test fixture duplicates the SUPPORTED_IPA_TYPES structure and can silently diverge from the real mapping over time; consider deriving it from SUPPORTED_IPA_TYPES at runtime (e.g. SUPPORTED_IPA_TYPES['yrzn001'].copy() or similar) and only overriding what needs to be controlled for the tests.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The local `_YRZN_MAPPING` test fixture duplicates the `SUPPORTED_IPA_TYPES` structure and can silently diverge from the real mapping over time; consider deriving it from `SUPPORTED_IPA_TYPES` at runtime (e.g. `SUPPORTED_IPA_TYPES['yrzn001'].copy()` or similar) and only overriding what needs to be controlled for the tests.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@ideaship ideaship left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Production bug exposed by the coverage gap (osism/tasks/conductor/utils.py)

_prepare_node_attributes() returns a 2-tuple (node_attributes, template_vars) (ironic.py:332). Two callers in utils.py still treat the return value as a dict:

  • _get_conductor_redfish_credentials (utils.py:223, then utils.py:227)
  • _get_conductor_redfish_address (utils.py:255, then utils.py:259)

Both do node_attributes = _prepare_node_attributes(...) and then if "driver_info" in node_attributes and node_attributes.get("driver") == "redfish":. Because node_attributes is now a tuple, "driver_info" in node_attributes is a tuple-membership test — is the string equal to one of the two dict elements? — which is always False. The if short-circuits before .get() is ever reached, so no exception is raised and nothing is logged: both functions silently return None, None / None regardless of the device's actual configuration.

What this actually breaks. These two helpers are the conductor-configuration fallback for Redfish, and they are the only source of credentials for the osism redfish list <hostname> <EthernetInterfaces|NetworkAdapters|NetworkDeviceFunctions> command. All three internal call sites call get_redfish_connection(hostname, ignore_ssl_errors=True) with no username/password, so:

  • if not username or not password: is always true → _get_conductor_redfish_credentials is invoked on every call to that command, and get_redfish_connection never reads credentials from Ironic driver_info (it only pulls redfish_address from there). With the bug, credentials always come back None, None, the BMC connection is built with SessionOrBasicAuth(username=None, password=None), and against any BMC that requires authentication the Redfish call fails, the exception is swallowed in redfish.py, and the command returns an empty list / "Could not establish Redfish connection" — with no hint that credentials were dropped. In practice the conductor-credential fallback is dead.
  • The address fallback (_get_conductor_redfish_address) likewise returns None, so base_url silently stays at the default https://{hostname} instead of the conductor-configured address. Lower impact (it degrades rather than hard-fails), but still wrong.

The tuple return was introduced in 0e66cb8, which changed ironic.py but never updated these two utils.py callers; test_utils.py has zero coverage of either function, so nothing in the suite catches it. This test-only PR cannot catch it by design — it never exercises the utils.py consumers — so flagging it for a separate fix.

Fix: unpack at both call sites — node_attributes, _ = _prepare_node_attributes(...) — and add a regression test for _get_conductor_redfish_credentials and _get_conductor_redfish_address.

assert tvars["remote_board_password"] == "password"


def test_prepare_template_vars_honor_node_secrets(prep):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_prepare_node_attributes has an explicit guard at ironic.py:213-215: node_secrets = device.custom_fields.get("secrets", {}) followed by if node_secrets is None: node_secrets = {}. That None branch handles a real NetBox state — the secrets custom field present but explicitly null, distinct from absent — and no test here passes custom_fields={"secrets": None}. Coverage shows it's the only line in the scoped helper region left uncovered. Please add a case with custom_fields={"secrets": None} to exercise it.

prep.deep_merge.assert_not_called()


def test_prepare_decrypts_and_merges_config_context_ironic_parameters(prep):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three "merge" tests (test_prepare_decrypts_and_merges_config_context_ironic_parameters, ..._custom_field_ironic_parameters, ..._merges_config_context_before_custom_field) patch deep_merge to a no-op MagicMock and only assert the overlay it was called with (call_args.args[1]) plus call order/count. They never assert the base argument (args[0]) is the node_attributes accumulator, nor that the returned attrs reflects a merged result. Since node_attributes is mutated in place and returned as attrs, a future wiring regression — base/overlay transposed, or merging into a stray copy — would pass these tests undetected. Today's wiring is correct, so this is a coverage gap, not a current bug. Suggest one integration-style case using the real deep_merge: a non-trivial base with config_context={"ironic_parameters": {"some_new_key": "v"}}, then assert the returned attrs contains both the original key and some_new_key.

# ---------------------------------------------------------------------------


# The yrzn001 mapping mirrored from the production SUPPORTED_IPA_TYPES so the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action needed — just flagging the trade-off: mirroring SUPPORTED_IPA_TYPES into a local _YRZN_MAPPING and patching the production constant with the copy keeps these tests isolated from future edits to the real dict, but it also means a production rename or typo in SUPPORTED_IPA_TYPES would not be caught here — the tests run against the frozen mirror.

@github-project-automation github-project-automation Bot moved this from Ready to In review in Human Board Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

Unit tests for osism/tasks/conductor/ironic.py — pure helpers

3 participants