Skip to content

T8989: static_routes module#13

Open
omnom62 wants to merge 5 commits into
mainfrom
T8989_static_routes
Open

T8989: static_routes module#13
omnom62 wants to merge 5 commits into
mainfrom
T8989_static_routes

Conversation

@omnom62

@omnom62 omnom62 commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Change Summary

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Code style update (formatting, renaming)
  • Refactoring (no functional changes)
  • Migration from an old Vyatta component to vyos-1x, please link to related PR inside obsoleted component
  • Other (please describe):

Related Task(s)

Related PR(s)

Component(s) name

Proposed changes

How to test

Test results

  • Sanity tests passed
  • Unit tests passed

Tested against VyOS versions:

  • 1.3.8
  • 1.4-rolling-202201010100

Checklist:

  • I have read the CONTRIBUTING document
  • I have linked this PR to one or more Phabricator Task(s)
  • I have run the ansible sanity and unit tests
  • My commit headlines contain a valid Task id
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have added unit tests to cover my changes
  • I have added a file to changelogs/fragments to describe the changes

@omnom62 omnom62 requested a review from a team as a code owner June 14, 2026 11:28
@omnom62 omnom62 requested review from andamasov, dmbaturin and gaige June 14, 2026 11:28
@omnom62 omnom62 marked this pull request as draft June 14, 2026 11:28
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 2f0a49a6-7de0-43a2-bf83-6b06222917c5

📥 Commits

Reviewing files that changed from the base of the PR and between 935b246 and 994544c.

📒 Files selected for processing (1)
  • changelogs/fragments/t8989_static_routes.yml
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • ansible/ansible (manual)
✅ Files skipped from review due to trivial changes (1)
  • changelogs/fragments/t8989_static_routes.yml
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Mergify Merge Protections
  • GitHub Check: Summary
🧰 Additional context used
🔍 Remote MCP Context7

Summary of Additional Context for PR Review

Based on the search results, here are key findings relevant to reviewing this vyos_static_routes module:

1. Ansible Module Idempotency Best Practices

The module implementation should follow standard idempotency patterns documented in Ansible:

  • Modules must check whether the desired final state has already been achieved and exit without performing redundant actions
  • The second run of an identical task should report changed: false and produce no commands
  • Review focus: The unit and integration tests verify idempotency (e.g., merged.yaml runs the same config twice and asserts changed == false on the second run), which aligns with Ansible best practices.

2. Check Mode Support Requirements

Modules supporting check_mode must:

  • Set supports_check_mode: True in the AnsibleModule configuration
  • Check module.check_mode to prevent actual system modifications during dry runs
  • Return the computed commands without applying them when in check mode
  • Review focus: The module's main() function includes the pattern: "In check mode, it returns the computed commands without applying them," which indicates proper check mode support.

3. HTTPAPI Plugin Architecture

For REST API-based modules like vyos_static_routes:

  • HTTPAPI plugins act as adapters for HTTP(S) APIs, enabling their use with the httpapi connection plugin
  • They should offer methods tailored to the specific API, including a send_request method for preparing and sending requests
  • The module uses ansible.netcommon.httpapi connection type in integration tests, which is the standard pattern.

4. Standard Integration Test Pattern

Network resource module integration tests should:

  • Test check mode behavior separately from actual execution
  • Run the same task twice to verify idempotency (first run shows changed: true, second run shows changed: false)
  • Include before/after state assertions and command generation verification
  • Review focus: The PR includes all state transitions (merged, replaced, deleted, gathered) with idempotency checks in dedicated test files, matching the recommended pattern.

5. Expected Test Coverage

Per Ansible documentation, new modules should be tested:

  • In check mode to verify no changes are applied
  • In normal mode for functionality
  • Again in normal mode to verify idempotency
  • Assessment: The PR includes 398 lines of unit tests plus integration tests covering all five state modes with idempotency verification, indicating comprehensive coverage.

Recommended Code Review Focus Areas

  1. Verify the module's check_mode handling prevents API calls when module.check_mode is True
  2. Confirm idempotency in the _route_cmds() function—ensure no commands are generated when desired and running state match
  3. Validate that the REST API calls via vyos.apply_commands() and vyos.save_config() are only executed in normal mode
  4. Ensure error handling covers both API failures and malformed configuration scenarios

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added vyos_static_routes module for managing IPv4 and IPv6 static routes via REST API, supporting merged, replaced, overridden, deleted, and gathered states.
  • Documentation

    • Added comprehensive module documentation for vyos_static_routes.
    • Updated README module list to reflect current available modules.
  • Tests

    • Added integration and unit tests for static routes functionality.

Walkthrough

Adds the vyos_static_routes Ansible module managing IPv4/IPv6 static routes via the VyOS HTTPS REST API. Includes argument schema, idempotent command generation across five state modes, unit tests, integration test scaffolding with four scenario playbooks, RST documentation, and a README update removing the unavailable vyos_prefix_lists entry.

Changes

vyos_static_routes module

Layer / File(s) Summary
Module metadata, schema, and constants
plugins/modules/vyos_static_routes.py
Defines DOCUMENTATION, EXAMPLES, RETURN, ARGUMENT_SPEC, module imports, and the STATIC_ROUTES_PATH constant forming the public contract.
Core logic: config reading, normalization, command generation, and main
plugins/modules/vyos_static_routes.py
Implements get_running_config, _normalize, _route_cmds, build_commands, and main; covers REST fetch, nested-dict normalization, per-destination idempotent set/delete command generation across all state modes, check-mode, and result assembly.
Unit test fixture and test suite
tests/unit/fixtures/static_routes_running.json, tests/unit/modules/test_vyos_static_routes.py
Adds the running-config JSON fixture and tests covering get_running_config parsing, _normalize, _route_cmds command paths, and build_commands for all state semantics including idempotency.
Integration test target scaffolding
tests/integration/targets/vyos_static_routes/aliases, tests/integration/targets/vyos_static_routes/defaults/main.yaml, tests/integration/targets/vyos_static_routes/vars/main.yaml, tests/integration/targets/vyos_static_routes/tasks/main.yaml, tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml, tests/integration/targets/vyos_static_routes/tests/httpapi.yaml
Adds the integration target alias, defaults, vars, task entry points, and the HTTPAPI test runner that discovers and loops over scenario files.
Integration test scenario playbooks
tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml, tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml, tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml, tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml, tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml, tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml
Adds shared config helpers and four scenario playbooks asserting change detection and idempotency for merged, deleted, gathered, and replaced states.
RST documentation and README update
docs/vyos.rest.vyos_static_routes_module.rst, README.md
Adds the full Sphinx RST page (synopsis, parameters, examples, return values, authorship) and updates the README modules table to list vyos_static_routes and remove the vyos_prefix_lists unavailable entry.
Changelog fragment
changelogs/fragments/t8989_static_routes.yml
Documents the module addition in the changelog.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is an unfilled template with no substantive content, but commit messages indicate the PR implements a static_routes module per task T8989. Complete the PR description template: detail the proposed changes (new module for managing VyOS static routes via REST API), list testing performed, and confirm checklist items.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: addition of a static_routes module for VyOS with comprehensive documentation and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch T8989_static_routes

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mergify

mergify Bot commented Jun 14, 2026

Copy link
Copy Markdown

Merge Protections

Your pull request matches the following merge protections and will not be merged until they are valid.

🔴 invalid-task-id label must be absent to merge

Waiting for

  • label != invalid-task-id
This rule is failing.

Block merge while the invalid-task-id label is present. Set by the per-repo product T-ID rule (product repos only); dormant where the label is never applied.

  • label != invalid-task-id

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🧹 Nitpick comments (5)
tests/integration/targets/vyos_static_routes/tests/httpapi.yaml (1)

2-21: ⚡ Quick win

Deduplicate the HTTPAPI runner to prevent drift.

Line 2–21 duplicates tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml. Keep one canonical runner and include it here to avoid subtle divergence over time.

Proposed refactor
 ---
-- name: Collect all httpapi test cases
-  ansible.builtin.find:
-    paths: "{{ role_path }}/tests/httpapi"
-    patterns: "{{ testcase }}.yaml"
-    use_regex: true
-  register: test_cases
-  delegate_to: localhost
-
-- name: Set test_items
-  ansible.builtin.set_fact:
-    test_items: "{{ test_cases.files | map(attribute='path') | list }}"
-
-- name: Run test case (connection=httpapi)
-  ansible.builtin.include_tasks: "{{ test_case_to_run }}"
-  vars:
-    ansible_connection: ansible.netcommon.httpapi
-    ansible_network_os: vyos.rest.vyos
-  with_items: "{{ test_items }}"
-  loop_control:
-    loop_var: test_case_to_run
+- name: Run shared httpapi runner
+  ansible.builtin.include_tasks: ../tasks/httpapi.yaml
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/targets/vyos_static_routes/tests/httpapi.yaml` around lines
2 - 21, The HTTPAPI test runner code in
tests/integration/targets/vyos_static_routes/tests/httpapi.yaml (the
ansible.builtin.find task for collecting test cases, the
ansible.builtin.set_fact task for setting test_items, and the
ansible.builtin.include_tasks loop) duplicates the canonical version in
tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml. Remove the
entire duplicated block and replace it with a single
ansible.builtin.include_tasks directive that references the canonical
tasks/httpapi.yaml file to maintain a single source of truth and prevent future
divergence.
tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml (1)

22-26: ⚡ Quick win

Strengthen idempotency assertion to verify no commands are produced.

At Line 22-26, the test only checks result.changed == false. Add result.commands == [] here too, so idempotency also guarantees zero command generation (consistent with merged/replaced scenarios).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml`
around lines 22 - 26, The "Assert idempotent" task in the deleted.yaml test only
verifies that result.changed equals false, but does not verify that no commands
were generated. Add an additional assertion to the that list checking that
result.commands equals an empty list to ensure complete idempotency
verification, making this assertion consistent with the merged and replaced test
scenarios.
tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml (1)

14-18: ⚡ Quick win

Gathered assertions are too broad for regression safety.

At Line 14-18, asserting only “any ipv4 exists” can miss normalization/field regressions. Consider asserting specific expected destinations/next-hop or blackhole fields (and IPv6 if _populate_config.yaml seeds it).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml`
around lines 14 - 18, The gathered.yaml test assertions are too permissive and
only verify that at least one IPv4 entry exists in the result.gathered list.
Replace or extend these assertions to validate specific field values such as
destinations, next-hop addresses, and blackhole settings in the gathered output
to catch normalization regressions. Additionally, verify IPv6 entries in the
assertions if the _populate_config.yaml test data includes IPv6 static routes.
This ensures the test validates actual data correctness, not just the presence
of generic entries.
plugins/modules/vyos_static_routes.py (2)

324-329: 💤 Low value

Consider validating distance range (1-255).

Documentation states admin distance is 1-255, but ARGUMENT_SPEC doesn't enforce this. Invalid values would reach the VyOS API.

If desired, Ansible supports inline validation via a type callback or explicit check in main(). Otherwise, rely on VyOS API validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/modules/vyos_static_routes.py` around lines 324 - 329, The distance
field in the blackhole_config options within ARGUMENT_SPEC does not validate
that the value is within the required range of 1-255 as per VyOS documentation.
Modify the distance dict entry in blackhole_config to include range validation
either by using an Ansible type callback (such as a custom validator) or by
adding an explicit validation check in the main() function that verifies the
distance value falls within 1-255 before passing it to the VyOS API.

218-218: 💤 Low value

Optional: Use list unpacking for path construction.

Ruff suggests [*_BASE, _ROUTE_KEY[afi], dest] over concatenation. Applies throughout _route_cmds and build_commands. Purely stylistic—current code is clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/modules/vyos_static_routes.py` at line 218, Replace list
concatenation with list unpacking syntax throughout the code. In the
`_route_cmds` and `build_commands` functions, change all occurrences of `_BASE +
[...]` patterns to use the unpacking syntax `[*_BASE, ...]` instead. This
stylistic change makes the code more idiomatic while maintaining identical
functionality.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/modules/vyos_static_routes.py`:
- Around line 226-229: The condition pattern in the _route_cmds function only
generates commands to set or update optional attributes but does not generate
delete commands when users omit attributes to restore VyOS defaults. In
plugins/modules/vyos_static_routes.py at lines 226-229, add an elif branch after
the existing if statement to check if dist is None and have_dist is not None,
then append a delete command using ("delete", bh_base + ["distance"]). In the
same file at lines 242-245, add a similar elif branch for next-hop distance
checking if dist is None and have_dist is not None, appending ("delete", nh_base
+ ["distance", str(have_dist)]). In the same file at lines 254-257, add an elif
branch for next-hop interface checking if iface is None and have_iface is not
None, appending ("delete", nh_base + ["interface"]).
- Around line 292-306: The replaced state detection at line 302 cannot identify
when next-hop attributes (distance, interface) are being cleared because
_route_cmds only generates commands for non-None values. When clearing an
attribute by setting it to None, _route_cmds produces no command, leaving
test_cmds empty even though the route should be recreated. Enhance the condition
that checks whether to delete and recreate the route to also compare next-hop
attributes directly between want_route and have_route (not just the commands
generated from them). Specifically, examine each next-hop in want_nhs and
have_nhs to detect when attributes like distance or interface differ between the
desired and current states, including cases where attributes are being removed
(transitioning from non-None to None).

In
`@tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml`:
- Line 19: Remove the `ignore_errors: true` directive from the populate fixture
configuration. This directive suppresses setup failures that should cause the
test to fail fast, which can hide actual module or API failures and allow
scenario assertions to pass incorrectly. Setup operations in the populate
fixture must not ignore errors so that idempotency testing remains reliable and
failures are caught immediately rather than being masked.

In `@tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml`:
- Around line 20-33: The test for the replaced operation does not actually
verify that the old routes were removed and only the new ones remain after the
replace action. Currently it only checks that the operation triggered a change
and subsequent runs are idempotent, which could pass even if the implementation
incorrectly merges routes instead of replacing them. Add a post-check assertion
after the idempotent assertion that gathers the actual static routes
configuration and validates that only the expected route set is present,
confirming the old routes have been removed. This could be done by adding a
gather or read-back task followed by an assertion comparing the gathered state
against the expected desired routes.

In `@tests/unit/modules/test_vyos_static_routes.py`:
- Around line 397-398: Remove the standalone unittest entrypoint block at the
end of the test_vyos_static_routes.py module. Delete the `if __name__ ==
"__main__":` block that calls `unittest.main()`. This ensures the module only
runs through pytest and pytest-xdist as intended by the collection test
guidelines, rather than allowing direct execution with the unittest runner.

---

Nitpick comments:
In `@plugins/modules/vyos_static_routes.py`:
- Around line 324-329: The distance field in the blackhole_config options within
ARGUMENT_SPEC does not validate that the value is within the required range of
1-255 as per VyOS documentation. Modify the distance dict entry in
blackhole_config to include range validation either by using an Ansible type
callback (such as a custom validator) or by adding an explicit validation check
in the main() function that verifies the distance value falls within 1-255
before passing it to the VyOS API.
- Line 218: Replace list concatenation with list unpacking syntax throughout the
code. In the `_route_cmds` and `build_commands` functions, change all
occurrences of `_BASE + [...]` patterns to use the unpacking syntax `[*_BASE,
...]` instead. This stylistic change makes the code more idiomatic while
maintaining identical functionality.

In `@tests/integration/targets/vyos_static_routes/tests/httpapi.yaml`:
- Around line 2-21: The HTTPAPI test runner code in
tests/integration/targets/vyos_static_routes/tests/httpapi.yaml (the
ansible.builtin.find task for collecting test cases, the
ansible.builtin.set_fact task for setting test_items, and the
ansible.builtin.include_tasks loop) duplicates the canonical version in
tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml. Remove the
entire duplicated block and replace it with a single
ansible.builtin.include_tasks directive that references the canonical
tasks/httpapi.yaml file to maintain a single source of truth and prevent future
divergence.

In `@tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml`:
- Around line 22-26: The "Assert idempotent" task in the deleted.yaml test only
verifies that result.changed equals false, but does not verify that no commands
were generated. Add an additional assertion to the that list checking that
result.commands equals an empty list to ensure complete idempotency
verification, making this assertion consistent with the merged and replaced test
scenarios.

In `@tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml`:
- Around line 14-18: The gathered.yaml test assertions are too permissive and
only verify that at least one IPv4 entry exists in the result.gathered list.
Replace or extend these assertions to validate specific field values such as
destinations, next-hop addresses, and blackhole settings in the gathered output
to catch normalization regressions. Additionally, verify IPv6 entries in the
assertions if the _populate_config.yaml test data includes IPv6 static routes.
This ensures the test validates actual data correctness, not just the presence
of generic entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 70efbff2-72ea-45ec-b333-f054d52225dc

📥 Commits

Reviewing files that changed from the base of the PR and between db8c9bd and 935b246.

📒 Files selected for processing (17)
  • README.md
  • docs/vyos.rest.vyos_static_routes_module.rst
  • plugins/modules/vyos_static_routes.py
  • tests/integration/targets/vyos_static_routes/aliases
  • tests/integration/targets/vyos_static_routes/defaults/main.yaml
  • tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml
  • tests/integration/targets/vyos_static_routes/tasks/main.yaml
  • tests/integration/targets/vyos_static_routes/tests/httpapi.yaml
  • tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml
  • tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml
  • tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml
  • tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml
  • tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml
  • tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml
  • tests/integration/targets/vyos_static_routes/vars/main.yaml
  • tests/unit/fixtures/static_routes_running.json
  • tests/unit/modules/test_vyos_static_routes.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • ansible/ansible (manual)
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: sanity / sanity-py3.11-2.19
  • GitHub Check: unit-source / py3.13 / stable-2.20
  • GitHub Check: Mergify Merge Protections
  • GitHub Check: Summary
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code using black with line length of 100 characters

Files:

  • plugins/modules/vyos_static_routes.py
  • tests/unit/modules/test_vyos_static_routes.py
**/test_*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use pytest with pytest-xdist (parallel execution with -n 2) for running collection tests

Files:

  • tests/unit/modules/test_vyos_static_routes.py
🪛 Ruff (0.15.15)
plugins/modules/vyos_static_routes.py

[warning] 218-218: Consider [*_BASE, _ROUTE_KEY[afi], dest] instead of concatenation

Replace with [*_BASE, _ROUTE_KEY[afi], dest]

(RUF005)


[warning] 223-223: Consider [*base, "blackhole"] instead of concatenation

Replace with [*base, "blackhole"]

(RUF005)


[warning] 229-229: Consider [*bh_base, "distance", str(dist)] instead of concatenation

Replace with [*bh_base, "distance", str(dist)]

(RUF005)


[warning] 236-236: Consider [*base, "next-hop", nh_addr] instead of concatenation

Replace with [*base, "next-hop", nh_addr]

(RUF005)


[warning] 245-245: Consider [*nh_base, "distance", str(dist)] instead of concatenation

Replace with [*nh_base, "distance", str(dist)]

(RUF005)


[warning] 250-250: Consider [*nh_base, "disable"] instead of concatenation

Replace with [*nh_base, "disable"]

(RUF005)


[warning] 252-252: Consider [*nh_base, "disable"] instead of concatenation

Replace with [*nh_base, "disable"]

(RUF005)


[warning] 257-257: Consider [*nh_base, "interface", iface] instead of concatenation

Replace with [*nh_base, "interface", iface]

(RUF005)


[warning] 269-269: Consider [*_BASE, route_key] instead of concatenation

Replace with [*_BASE, route_key]

(RUF005)


[warning] 275-275: Consider [*_BASE, route_key, route["dest"]] instead of concatenation

Replace with [*_BASE, route_key, route["dest"]]

(RUF005)


[warning] 287-287: Consider [*_BASE, route_key, dest] instead of concatenation

Replace with [*_BASE, route_key, dest]

(RUF005)


[warning] 303-303: Consider [*_BASE, route_key, dest] instead of concatenation

Replace with [*_BASE, route_key, dest]

(RUF005)

🔍 Remote MCP Context7

Based on my search of available documentation, I can now provide relevant context for reviewing this pull request:

Additional Context for PR Review

VyOS Static Routes Configuration Standards

The VyOS documentation confirms support for IPv4 and IPv6 static routes with next-hop configuration, including administrative distance parameters (allowed range 1-255). Specifically:

  • IPv4 unicast routes are configured with: set protocols static route <subnet> next-hop <address>
  • IPv6 unicast routes are configured with: set protocols static route6 <subnet> next-hop <address>
  • Blackhole routes for IPv4 are configured with: set protocols static route <subnet> blackhole
  • Blackhole routes for IPv6 are configured with: set protocols static route6 <subnet> blackhole
  • Administrative distance for blackhole routes is configured with: set protocols static route6 <subnet> blackhole distance <distance>

Validation note: The module's handling of IPv4/IPv6 AFI separation, next-hop configuration, and blackhole distance parameters aligns with VyOS CLI structure as documented.

Ansible Network Module State Handling Patterns

Ansible network resource modules should support the following state values: merged, replaced, overridden, deleted, and gathered. The PR implementation supports all five states:

  • merged - Ansible merges the on-device configuration with the provided configuration
  • replaced - Ansible replaces the on-device configuration subsection with the provided configuration subsection
  • overridden - Ansible overrides the on-device configuration for the resource with the provided configuration
  • deleted - Ansible deletes the on-device configuration subsection and restores any default settings
  • The module also supports gathered state for reading current configuration

Key review point: The module's state handling matches Ansible conventions. Verify in code review that replaced mode properly handles the replace semantics (replacing a specific subsection rather than the entire resource), and that overridden deletes extra routes as expected.

Test Coverage Assessment

The PR includes comprehensive test coverage:

  • Integration tests covering all five state transitions (merged, replaced, deleted, gathered) with idempotency verification
  • Unit tests validating core logic: config parsing, command generation, and state-specific behavior
  • Fixtures for both IPv4 and IPv6 static routes with blackhole and next-hop configurations

Recommended focus areas for code review:

  1. Verify the replaced state correctly implements subsection-level replacement rather than full resource replacement
  2. Validate idempotency of command generation in _route_cmds() function
  3. Check that the module correctly normalizes running configuration for comparison with desired state
  4. Ensure error handling for malformed configurations or API failures,
🔇 Additional comments (13)
tests/unit/fixtures/static_routes_running.json (1)

1-21: LGTM!

tests/unit/modules/test_vyos_static_routes.py (1)

1-396: LGTM!

tests/integration/targets/vyos_static_routes/aliases (1)

1-1: LGTM!

tests/integration/targets/vyos_static_routes/defaults/main.yaml (1)

1-3: LGTM!

tests/integration/targets/vyos_static_routes/vars/main.yaml (1)

1-1: LGTM!

tests/integration/targets/vyos_static_routes/tasks/main.yaml (1)

2-5: LGTM!

tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml (1)

2-21: LGTM!

tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml (1)

2-5: LGTM!

tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml (1)

7-37: LGTM!

plugins/modules/vyos_static_routes.py (4)

1-143: LGTM!


146-189: LGTM!


192-212: LGTM!


353-386: LGTM!

Comment on lines +226 to +229
dist = (want_route["blackhole_config"] or {}).get("distance")
have_dist = (have_route.get("blackhole_config") or {}).get("distance")
if dist is not None and dist != have_dist:
cmds.append(("set", bh_base + ["distance", str(dist)]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

_route_cmds cannot clear optional attributes when user omits them.

The pattern if want_val is not None and want_val != have_val only handles setting/updating values, not clearing them. When user omits an optional attribute (distance, admin_distance, interface) to restore VyOS defaults, existing values persist because no delete command is generated.

  • plugins/modules/vyos_static_routes.py#L226-L229: Add elif dist is None and have_dist is not None branch to delete blackhole distance.
  • plugins/modules/vyos_static_routes.py#L242-L245: Add elif dist is None and have_dist is not None branch to delete next-hop distance.
  • plugins/modules/vyos_static_routes.py#L254-L257: Add elif iface is None and have_iface is not None branch to delete next-hop interface.
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 229-229: Consider [*bh_base, "distance", str(dist)] instead of concatenation

Replace with [*bh_base, "distance", str(dist)]

(RUF005)

📍 Affects 1 file
  • plugins/modules/vyos_static_routes.py#L226-L229 (this comment)
  • plugins/modules/vyos_static_routes.py#L242-L245
  • plugins/modules/vyos_static_routes.py#L254-L257
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/modules/vyos_static_routes.py` around lines 226 - 229, The condition
pattern in the _route_cmds function only generates commands to set or update
optional attributes but does not generate delete commands when users omit
attributes to restore VyOS defaults. In plugins/modules/vyos_static_routes.py at
lines 226-229, add an elif branch after the existing if statement to check if
dist is None and have_dist is not None, then append a delete command using
("delete", bh_base + ["distance"]). In the same file at lines 242-245, add a
similar elif branch for next-hop distance checking if dist is None and have_dist
is not None, appending ("delete", nh_base + ["distance", str(have_dist)]). In
the same file at lines 254-257, add an elif branch for next-hop interface
checking if iface is None and have_iface is not None, appending ("delete",
nh_base + ["interface"]).

Comment on lines +292 to +306
if state == "replaced" and dest in have_afi:
# check if anything differs before deleting
test_cmds = _route_cmds(afi, dest, want_route, have_route)
want_nhs = set(want_route.get("next_hops") or {})
have_nhs = set(have_route.get("next_hops") or {})
extra_nhs = have_nhs - want_nhs
want_bh = want_route.get("blackhole_config")
have_bh = have_route.get("blackhole_config")
have_bh_now = have_bh is not None
want_bh_now = want_bh is not None
if test_cmds or extra_nhs or have_bh_now != want_bh_now:
cmds.append(("delete", _BASE + [route_key, dest]))
have_route = {}
else:
continue # already matches — idempotent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify replaced state behavior is tested with attribute clearing scenarios
rg -n "replaced" tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml -A 20 | head -50

Repository: vyos/rest.vyos

Length of output: 1021


🏁 Script executed:

#!/bin/bash
# View the actual code at lines 292-306 with context
sed -n '280,320p' plugins/modules/vyos_static_routes.py

Repository: vyos/rest.vyos

Length of output: 1616


🏁 Script executed:

#!/bin/bash
# Find and examine the _route_cmds function
rg -n "def _route_cmds" plugins/modules/vyos_static_routes.py -A 30

Repository: vyos/rest.vyos

Length of output: 1394


🏁 Script executed:

#!/bin/bash
# Check the full replaced.yaml test file to see if it covers attribute clearing
cat tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml

Repository: vyos/rest.vyos

Length of output: 942


🏁 Script executed:

#!/bin/bash
# Check if _route_cmds has delete command generation (search full function)
rg -n "def _route_cmds" plugins/modules/vyos_static_routes.py -A 60 | head -80

Repository: vyos/rest.vyos

Length of output: 2585


🏁 Script executed:

#!/bin/bash
# Check for delete commands in the module
rg -n '"delete"' plugins/modules/vyos_static_routes.py | head -20

Repository: vyos/rest.vyos

Length of output: 420


🏁 Script executed:

#!/bin/bash
# Look for tests that clear/remove attributes in replaced.yaml or related files
find tests -name "*.yaml" -exec grep -l "replaced" {} \; | xargs grep -l "distance" 2>/dev/null || true

Repository: vyos/rest.vyos

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check module documentation or comments for delete attribute examples
rg -i "delete.*distance|distance.*delete" plugins/modules/ -A 2 -B 2

Repository: vyos/rest.vyos

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check if there are other test scenarios for replaced state
find tests -name "*.yaml" | xargs grep -l "state.*replaced" | xargs wc -l | sort -rn | head -5

Repository: vyos/rest.vyos

Length of output: 375


🏁 Script executed:

#!/bin/bash
# Look at the full replaced.yaml to see all test cases
wc -l tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml

Repository: vyos/rest.vyos

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Check the full replaced.yaml file
cat tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml | head -150

Repository: vyos/rest.vyos

Length of output: 942


replaced state cannot detect when next-hop attributes (distance, interface) are cleared.

The replaced detection at line 302 checks test_cmds to decide whether to delete and recreate the route. However, _route_cmds (lines 215–259) only generates commands when attributes have non-None values. Setting distance or interface to None generates no command, so test_cmds remains empty even when the intent is to clear those attributes. Consequently, the replaced condition evaluates to false, and the route is not deleted/recreated.

The integration test in replaced.yaml (35 lines) covers only adding/replacing next-hops, not clearing attributes—this scenario is untested.

🧰 Tools
🪛 Ruff (0.15.15)

[warning] 303-303: Consider [*_BASE, route_key, dest] instead of concatenation

Replace with [*_BASE, route_key, dest]

(RUF005)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/modules/vyos_static_routes.py` around lines 292 - 306, The replaced
state detection at line 302 cannot identify when next-hop attributes (distance,
interface) are being cleared because _route_cmds only generates commands for
non-None values. When clearing an attribute by setting it to None, _route_cmds
produces no command, leaving test_cmds empty even though the route should be
recreated. Enhance the condition that checks whether to delete and recreate the
route to also compare next-hop attributes directly between want_route and
have_route (not just the commands generated from them). Specifically, examine
each next-hop in want_nhs and have_nhs to detect when attributes like distance
or interface differ between the desired and current states, including cases
where attributes are being removed (transitioning from non-None to None).

next_hops:
- forward_router_address: 2001:db8::1
state: merged
ignore_errors: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not suppress setup failures in populate fixture.

Line 19 (ignore_errors: true) can hide module/API failures and make scenario assertions pass for the wrong reason. Setup must fail fast for reliable idempotency testing.

Proposed fix
 - name: Populate static_routes config for testing
   vyos.rest.vyos_static_routes:
@@
     state: merged
-  ignore_errors: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ignore_errors: true
- name: Populate static_routes config for testing
vyos.rest.vyos_static_routes:
state: merged
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml`
at line 19, Remove the `ignore_errors: true` directive from the populate fixture
configuration. This directive suppresses setup failures that should cause the
test to fail fast, which can hide actual module or API failures and allow
scenario assertions to pass incorrectly. Setup operations in the populate
fixture must not ignore errors so that idempotency testing remains reliable and
failures are caught immediately rather than being masked.

Comment on lines +20 to +33
- assert:
that:
- result.changed == true

- name: Replace static_routes configuration (IDEMPOTENT)
register: result
vyos.rest.vyos_static_routes: *id001

- name: Assert idempotent
assert:
that:
- result.changed == false
- result.commands == []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

replaced semantics are not actually verified.

At Line 20-33, the scenario validates change/idempotency but never proves old routes were removed after replace. This can pass even if implementation behaves like merge. Add a post-check (e.g., gathered/read-back assertion) confirming only the declared route set remains.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml`
around lines 20 - 33, The test for the replaced operation does not actually
verify that the old routes were removed and only the new ones remain after the
replace action. Currently it only checks that the operation triggered a change
and subsequent runs are idempotent, which could pass even if the implementation
incorrectly merges routes instead of replacing them. Add a post-check assertion
after the idempotent assertion that gathers the actual static routes
configuration and validates that only the expected route set is present,
confirming the old routes have been removed. This could be done by adding a
gather or read-back task followed by an assertion comparing the gathered state
against the expected desired routes.

Source: MCP tools

Comment on lines +397 to +398
if __name__ == "__main__":
unittest.main()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the standalone unittest entrypoint in this collection test module.

if __name__ == "__main__": unittest.main() is unnecessary here and encourages running outside the
pytest-xdist path used for collection tests.

Suggested patch
-if __name__ == "__main__":
-    unittest.main()

As per coding guidelines: **/test_*.py: Use pytest with pytest-xdist (parallel execution with -n 2) for running collection tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if __name__ == "__main__":
unittest.main()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/modules/test_vyos_static_routes.py` around lines 397 - 398, Remove
the standalone unittest entrypoint block at the end of the
test_vyos_static_routes.py module. Delete the `if __name__ == "__main__":` block
that calls `unittest.main()`. This ensures the module only runs through pytest
and pytest-xdist as intended by the collection test guidelines, rather than
allowing direct execution with the unittest runner.

Source: Coding guidelines

@omnom62 omnom62 marked this pull request as ready for review June 14, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

1 participant