diff --git a/docs/superpowers/plans/2026-04-16-easy-wins-plan.md b/docs/superpowers/plans/2026-04-16-easy-wins-plan.md
new file mode 100644
index 00000000..b3697382
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-16-easy-wins-plan.md
@@ -0,0 +1,1229 @@
+# Easy Wins Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Five sequential quality improvements to the vyos.vyos Ansible collection — formatting, bugfix, dead code removal, test coverage, template deduplication.
+
+**Architecture:** Each phase is an independent branch and PR. Phases are ordered by risk: mechanical formatting first, then a one-line bugfix, then dead code cleanup, then new tests, then the riskiest refactor (template deduplication) last with full test coverage in place.
+
+**Tech Stack:** Python 3, Ansible (netcommon), pytest, black, isort, flake8
+
+**Spec:** `docs/superpowers/specs/2026-04-12-easy-wins-design.md`
+
+---
+
+## Phase 1: Formatting Compliance
+
+### Task 1: Run black formatter across entire codebase
+
+**Files:**
+- Modify: ~193 Python files across `plugins/` and `tests/`
+
+- [ ] **Step 1: Create branch**
+
+```bash
+git checkout -b fix/formatting-compliance main
+```
+
+- [ ] **Step 2: Run black formatter**
+
+```bash
+black .
+```
+
+Expected: "reformatted N files" (approximately 193 files).
+
+- [ ] **Step 3: Verify black passes**
+
+```bash
+black --check .
+```
+
+Expected: "All done!" with exit code 0.
+
+- [ ] **Step 4: Commit black changes**
+
+```bash
+git add -A
+git commit -m "style: apply black formatting across entire codebase"
+```
+
+### Task 2: Fix isort violations
+
+**Files:**
+- Modify: `plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py`
+- Modify: `plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py`
+- Modify: `plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py`
+
+Note: `plugins/module_utils/network/vyos/utils/version.py` was flagged by isort but the `LooseVersion` import is a deliberate re-export used by 19 other files. The `# pylint: disable=unused-import` comment is correct. Do NOT remove this import.
+
+- [ ] **Step 1: Run isort**
+
+```bash
+isort .
+```
+
+- [ ] **Step 2: Verify isort passes**
+
+```bash
+isort --check-only .
+```
+
+Expected: exit code 0, no violations.
+
+- [ ] **Step 3: Commit isort changes**
+
+```bash
+git add -A
+git commit -m "style: fix isort import ordering violations"
+```
+
+### Task 3: Run full verification and add changelog
+
+**Files:**
+- Create: `changelogs/fragments/formatting-compliance.yml`
+
+- [ ] **Step 1: Run full lint suite**
+
+```bash
+black --check . && isort --check-only . && flake8 .
+```
+
+Expected: All three pass with zero issues. If flake8 reports any issues, fix them before proceeding.
+
+- [ ] **Step 2: Run unit tests to confirm no regressions**
+
+```bash
+pytest tests/unit -vvv -n 2
+```
+
+Expected: All tests pass.
+
+- [ ] **Step 3: Create changelog fragment**
+
+Create `changelogs/fragments/formatting-compliance.yml`:
+
+```yaml
+---
+minor_changes:
+ - Collection-wide formatting compliance with black and isort.
+```
+
+- [ ] **Step 4: Commit changelog**
+
+```bash
+git add changelogs/fragments/formatting-compliance.yml
+git commit -m "chore: add changelog fragment for formatting compliance"
+```
+
+---
+
+## Phase 2: Fix meta/runtime.yml Redirect
+
+### Task 4: Fix snmp_server redirect typo
+
+**Files:**
+- Modify: `meta/runtime.yml:58`
+- Create: `changelogs/fragments/fix-snmp-server-redirect.yml`
+
+- [ ] **Step 1: Create branch**
+
+```bash
+git checkout -b fix/snmp-server-redirect main
+```
+
+- [ ] **Step 2: Verify the bug exists**
+
+```bash
+grep "vyos_snmp_servers" meta/runtime.yml
+```
+
+Expected output: ` redirect: vyos.vyos.vyos_snmp_servers`
+
+Verify the correct module exists:
+
+```bash
+ls plugins/modules/vyos_snmp_server.py
+```
+
+Expected: file exists.
+
+```bash
+ls plugins/modules/vyos_snmp_servers.py 2>/dev/null; echo "exit: $?"
+```
+
+Expected: file does not exist, exit code non-zero.
+
+- [ ] **Step 3: Fix the redirect**
+
+In `meta/runtime.yml`, change line 58 from:
+
+```yaml
+ redirect: vyos.vyos.vyos_snmp_servers
+```
+
+to:
+
+```yaml
+ redirect: vyos.vyos.vyos_snmp_server
+```
+
+- [ ] **Step 4: Check for other references to the plural form**
+
+```bash
+grep -r "vyos_snmp_servers" . --include="*.py" --include="*.yml" --include="*.yaml" | grep -v "docs/superpowers"
+```
+
+Expected: Only `meta/runtime.yml` should match (now fixed). If other files reference the plural form, fix those too.
+
+- [ ] **Step 5: Create changelog fragment**
+
+Create `changelogs/fragments/fix-snmp-server-redirect.yml`:
+
+```yaml
+---
+bugfixes:
+ - Fix meta/runtime.yml redirect for snmp_server pointing to non-existent vyos_snmp_servers module.
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add meta/runtime.yml changelogs/fragments/fix-snmp-server-redirect.yml
+git commit -m "fix: correct snmp_server redirect in meta/runtime.yml"
+```
+
+---
+
+## Phase 3: Deprecated Feature Cleanup
+
+### Task 5: Remove pre-1.3 commented-out code from vyos_bgp_global
+
+**Files:**
+- Modify: `plugins/modules/vyos_bgp_global.py`
+
+- [ ] **Step 1: Create branch**
+
+```bash
+git checkout -b chore/deprecated-cleanup main
+```
+
+- [ ] **Step 2: Identify all commented-out pre-1.3 blocks**
+
+The following commented-out blocks in `plugins/modules/vyos_bgp_global.py` are pre-1.3 artifacts and must be removed. Each block starts with a `#` comment containing one of these markers:
+- "Moved to address-family before 1.3"
+- "Removed before 1.3"
+- "Removed prior to 1.3"
+
+Remove these complete commented-out blocks (the marker line AND all indented commented lines that follow as part of that parameter's YAML documentation block):
+
+1. Lines ~86-89: `allowas_in` — "Moved to address-family before 1.3"
+2. Lines ~90-93: `as_override` — "Moved to address-family before 1.3"
+3. Lines ~94-107: `attribute_unchanged` — "Moved to address-family before 1.3" (includes suboptions)
+4. Lines ~121-127: `orf` — "Removed before 1.3" (includes choices)
+5. Lines ~149-161: `distribute_list` — "Moved to address-family before 1.3" (includes suboptions)
+6. Lines ~166-190: `interface` comment block — "added in 1.3" (this is a commented-out description of a feature, not active code)
+7. Lines ~191-202: `filter_list` — "Moved to address-family before 1.3" (includes suboptions)
+8. Lines ~206-212: `maximum_prefix` and `nexthop_self` — "Moved to address-family before 1.3"
+9. Lines ~231-242: `prefix_list` — "Moved to address-family before 1.3" (includes suboptions)
+10. Lines ~246-248: `remove_private_as` — "Moved to address-family before 1.3"
+11. Lines ~249-260: `route_map` — "Moved to address-family before 1.3" (includes suboptions)
+12. Lines ~261-263: `route_reflector_client` — "Moved to address-family before 1.3"
+13. Lines ~264-266: `route_server_client` — "Removed prior to 1.3"
+14. Lines ~270-272: `soft_reconfiguration` — "Moved to address-family before 1.3"
+15. Lines ~279-281: `unsuppress_map` — "Moved to address-family before 1.3"
+16. Lines ~283-285: `weight` — "Moved to address-family before 1.3"
+
+**Important:** Do NOT remove the `# <-- added in 1.3` inline comment on `solo` (line ~273) — that's an active parameter notation, not dead code.
+
+- [ ] **Step 3: Remove all identified blocks**
+
+Open `plugins/modules/vyos_bgp_global.py` and remove all 16 blocks listed above. After removal, the YAML docstring should flow cleanly from `capability:` → `extended_nexthop:` directly to `default_originate:`, and from `port:` directly to `remote_as:`, etc.
+
+- [ ] **Step 4: Verify removal is complete**
+
+```bash
+grep -n "Removed before 1.3\|Removed prior to 1.3\|Moved to address-family before 1.3" plugins/modules/vyos_bgp_global.py
+```
+
+Expected: No output (all markers removed).
+
+- [ ] **Step 5: Run unit tests**
+
+```bash
+pytest tests/unit/modules/network/vyos/test_vyos_bgp_global.py -vvv
+```
+
+Expected: All tests pass. These tests exercise config/facts, not DOCUMENTATION strings, so removing commented-out YAML should have no effect.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add plugins/modules/vyos_bgp_global.py
+git commit -m "chore: remove commented-out pre-1.3 parameter artifacts from vyos_bgp_global docs"
+```
+
+### Task 6: Verify tombstoned modules have no stale code
+
+**Files:**
+- Possibly modify: any stale `vyos_logging.py` files (if found)
+
+- [ ] **Step 1: Check for stale logging module code**
+
+`meta/runtime.yml` tombstones `logging` and `vyos_logging` (removal_version 6.0.0). Verify no module files exist:
+
+```bash
+ls plugins/modules/vyos_logging.py 2>/dev/null; echo "exit: $?"
+find plugins/ -name "*vyos_logging*" -not -name "*logging_global*" | head -20
+```
+
+Expected: No files found (the tombstoned modules are already removed and replaced by `vyos_logging_global`).
+
+- [ ] **Step 2: Check for stale vrf.old file**
+
+A file `plugins/module_utils/network/vyos/config/vrf/vrf.old` is tracked in git. This appears to be a leftover from development:
+
+```bash
+head -5 plugins/module_utils/network/vyos/config/vrf/vrf.old
+```
+
+If it's a backup/old version of `vrf.py`, remove it:
+
+```bash
+git rm plugins/module_utils/network/vyos/config/vrf/vrf.old
+```
+
+- [ ] **Step 3: Verify deprecation markers on v7.0.0-targeted features**
+
+These features are deprecated but still needed for VyOS 1.3.8 support. Verify each has correct deprecation documentation (read-only, no changes unless text is wrong):
+
+```bash
+grep -n -A2 "Deprecated" plugins/modules/vyos_firewall_interfaces.py | head -10
+grep -n -A2 "Deprecated\|Unavailable after 1.4" plugins/modules/vyos_bgp_global.py | head -10
+grep -n -A2 "Deprecated\|Unavailable after 1.4" plugins/modules/vyos_vrf.py | head -10
+grep -n "removed_in_version" plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py
+```
+
+Expected: Each prints existing deprecation markers. No changes needed if text is accurate.
+
+- [ ] **Step 4: Create changelog fragment**
+
+Create `changelogs/fragments/deprecated-cleanup.yml`:
+
+```yaml
+---
+minor_changes:
+ - vyos_bgp_global - remove commented-out pre-1.3 deprecated parameter documentation artifacts.
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A
+git commit -m "chore: remove stale pre-1.3 artifacts and verify deprecation markers"
+```
+
+---
+
+## Phase 4: Missing Unit Tests
+
+### Task 7: Add unit tests for vyos_l3_interfaces
+
+**Files:**
+- Create: `tests/unit/modules/network/vyos/fixtures/vyos_l3_interfaces_config.cfg`
+- Create: `tests/unit/modules/network/vyos/test_vyos_l3_interfaces.py`
+
+- [ ] **Step 1: Create branch**
+
+```bash
+git checkout -b test/missing-unit-tests main
+```
+
+- [ ] **Step 2: Create fixture file**
+
+Create `tests/unit/modules/network/vyos/fixtures/vyos_l3_interfaces_config.cfg`:
+
+```
+set interfaces ethernet eth0 address 'dhcp'
+set interfaces ethernet eth1 address '192.0.2.14/24'
+set interfaces ethernet eth2 address '192.0.2.10/24'
+set interfaces ethernet eth2 address '2001:db8::10/32'
+set interfaces ethernet eth3 address '198.51.100.10/24'
+set interfaces ethernet eth3 vif 101 address '198.51.100.130/25'
+set interfaces ethernet eth3 vif 102 address '2001:db8:4000::3/34'
+set interfaces loopback lo
+```
+
+- [ ] **Step 3: Create test file**
+
+Create `tests/unit/modules/network/vyos/test_vyos_l3_interfaces.py`:
+
+```python
+# (c) 2016 Red Hat Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_l3_interfaces
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosL3InterfacesModule(TestVyosModule):
+ module = vyos_l3_interfaces
+
+ def setUp(self):
+ super(TestVyosL3InterfacesModule, self).setUp()
+ self.mock_get_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.get_config",
+ )
+ self.get_config = self.mock_get_config.start()
+
+ self.mock_load_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.load_config",
+ )
+ self.load_config = self.mock_load_config.start()
+
+ self.mock_get_resource_connection_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base.get_resource_connection",
+ )
+ self.get_resource_connection_config = self.mock_get_resource_connection_config.start()
+
+ self.mock_get_resource_connection_facts = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection",
+ )
+ self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start()
+
+ self.mock_execute_show_command = patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos."
+ "facts.l3_interfaces.l3_interfaces.L3_interfacesFacts.get_device_data",
+ )
+ self.execute_show_command = self.mock_execute_show_command.start()
+ self.fixture_path = "vyos_l3_interfaces_config.cfg"
+
+ def tearDown(self):
+ super(TestVyosL3InterfacesModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_get_config.stop()
+ self.mock_load_config.stop()
+ self.mock_execute_show_command.stop()
+
+ def load_fixtures(self, commands=None, filename=None):
+ def load_from_file(*args, **kwargs):
+ return load_fixture(self.fixture_path)
+
+ self.execute_show_command.side_effect = load_from_file
+
+ def test_vyos_l3_interfaces_merged(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ ipv4=[dict(address="192.0.2.15/24")],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set interfaces ethernet eth1 address '192.0.2.15/24'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_merged_idempotent(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ ipv4=[dict(address="192.0.2.14/24")],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_l3_interfaces_merged_ipv6(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ ipv6=[dict(address="2001:db8::1/64")],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set interfaces ethernet eth1 address '2001:db8::1/64'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_replaced(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth2",
+ ipv4=[dict(address="203.0.113.1/24")],
+ ),
+ ],
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete interfaces ethernet eth2 address '192.0.2.10/24'",
+ "delete interfaces ethernet eth2 address '2001:db8::10/32'",
+ "set interfaces ethernet eth2 address '203.0.113.1/24'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_deleted(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(name="eth1"),
+ ],
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete interfaces ethernet eth1 address '192.0.2.14/24'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_gathered(self):
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False, commands=[])
+ self.assertIn("gathered", result)
+
+ def test_vyos_l3_interfaces_parsed(self):
+ parsed_cfg = (
+ "set interfaces ethernet eth1 address '192.0.2.14/24'\n"
+ "set interfaces ethernet eth2 address '192.0.2.10/24'"
+ )
+ set_module_args(dict(state="parsed", running_config=parsed_cfg))
+ result = self.execute_module(changed=False, commands=[])
+ self.assertIn("parsed", result)
+
+ def test_vyos_l3_interfaces_vif_merged(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth3",
+ vifs=[
+ dict(
+ vlan_id=101,
+ ipv4=[dict(address="198.51.100.131/25")],
+ ),
+ ],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set interfaces ethernet eth3 vif 101 address '198.51.100.131/25'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+```
+
+- [ ] **Step 4: Run the tests**
+
+```bash
+pytest tests/unit/modules/network/vyos/test_vyos_l3_interfaces.py -vvv
+```
+
+Expected: All tests pass. If any fail, examine the fixture data and expected commands — the module's facts parser and config builder determine what commands get generated. Adjust fixture data or expected commands to match.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add tests/unit/modules/network/vyos/test_vyos_l3_interfaces.py tests/unit/modules/network/vyos/fixtures/vyos_l3_interfaces_config.cfg
+git commit -m "test: add unit tests for vyos_l3_interfaces module"
+```
+
+### Task 8: Add unit tests for vyos_lldp_interfaces
+
+**Files:**
+- Create: `tests/unit/modules/network/vyos/fixtures/vyos_lldp_interfaces_config.cfg`
+- Create: `tests/unit/modules/network/vyos/test_vyos_lldp_interfaces.py`
+
+- [ ] **Step 1: Create fixture file**
+
+Create `tests/unit/modules/network/vyos/fixtures/vyos_lldp_interfaces_config.cfg`:
+
+```
+set service lldp interface eth1 location elin '0000000911'
+set service lldp interface eth2 location coordinate-based altitude '2200'
+set service lldp interface eth2 location coordinate-based datum 'WGS84'
+set service lldp interface eth2 location coordinate-based latitude '33.524449N'
+set service lldp interface eth2 location coordinate-based longitude '222.267255W'
+```
+
+- [ ] **Step 2: Create test file**
+
+Create `tests/unit/modules/network/vyos/test_vyos_lldp_interfaces.py`:
+
+```python
+# (c) 2016 Red Hat Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_lldp_interfaces
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosLldpInterfacesModule(TestVyosModule):
+ module = vyos_lldp_interfaces
+
+ def setUp(self):
+ super(TestVyosLldpInterfacesModule, self).setUp()
+ self.mock_get_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.get_config",
+ )
+ self.get_config = self.mock_get_config.start()
+
+ self.mock_load_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.load_config",
+ )
+ self.load_config = self.mock_load_config.start()
+
+ self.mock_get_resource_connection_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base.get_resource_connection",
+ )
+ self.get_resource_connection_config = self.mock_get_resource_connection_config.start()
+
+ self.mock_get_resource_connection_facts = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection",
+ )
+ self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start()
+
+ self.mock_execute_show_command = patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos."
+ "facts.lldp_interfaces.lldp_interfaces.Lldp_interfacesFacts.get_device_data",
+ )
+ self.execute_show_command = self.mock_execute_show_command.start()
+ self.fixture_path = "vyos_lldp_interfaces_config.cfg"
+
+ def tearDown(self):
+ super(TestVyosLldpInterfacesModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_get_config.stop()
+ self.mock_load_config.stop()
+ self.mock_execute_show_command.stop()
+
+ def load_fixtures(self, commands=None, filename=None):
+ def load_from_file(*args, **kwargs):
+ return load_fixture(self.fixture_path)
+
+ self.execute_show_command.side_effect = load_from_file
+
+ def test_vyos_lldp_interfaces_merged_elin(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth3",
+ location=dict(elin="9911"),
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set service lldp interface eth3 location elin '9911'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_lldp_interfaces_merged_idempotent(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ location=dict(elin="0000000911"),
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_lldp_interfaces_merged_coordinate(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth3",
+ location=dict(
+ coordinate_based=dict(
+ altitude=1000,
+ datum="WGS84",
+ latitude="40.0N",
+ longitude="74.0W",
+ ),
+ ),
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set service lldp interface eth3 location coordinate-based altitude '1000'",
+ "set service lldp interface eth3 location coordinate-based datum 'WGS84'",
+ "set service lldp interface eth3 location coordinate-based latitude '40.0N'",
+ "set service lldp interface eth3 location coordinate-based longitude '74.0W'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_lldp_interfaces_replaced(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ location=dict(elin="1234567890"),
+ ),
+ ],
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete service lldp interface eth1 location elin '0000000911'",
+ "set service lldp interface eth1 location elin '1234567890'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_lldp_interfaces_deleted(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(name="eth1"),
+ ],
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete service lldp interface eth1",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_lldp_interfaces_gathered(self):
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False, commands=[])
+ self.assertIn("gathered", result)
+
+ def test_vyos_lldp_interfaces_parsed(self):
+ parsed_cfg = "set service lldp interface eth1 location elin '0000000911'"
+ set_module_args(dict(state="parsed", running_config=parsed_cfg))
+ result = self.execute_module(changed=False, commands=[])
+ self.assertIn("parsed", result)
+```
+
+- [ ] **Step 3: Run the tests**
+
+```bash
+pytest tests/unit/modules/network/vyos/test_vyos_lldp_interfaces.py -vvv
+```
+
+Expected: All tests pass. Adjust fixture data or expected commands if the facts parser returns different structures than expected.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add tests/unit/modules/network/vyos/test_vyos_lldp_interfaces.py tests/unit/modules/network/vyos/fixtures/vyos_lldp_interfaces_config.cfg
+git commit -m "test: add unit tests for vyos_lldp_interfaces module"
+```
+
+### Task 9: Add unit tests for vyos_vlan (legacy module)
+
+**Files:**
+- Create: `tests/unit/modules/network/vyos/test_vyos_vlan.py`
+
+Note: `vyos_vlan` is a legacy module — it uses `get_config`/`load_config`/`run_commands` directly, not the resource module pattern. It uses `present`/`absent` states. The test pattern follows `test_vyos_banner.py`, not the resource module tests. This module also calls `run_commands` to do `show interfaces` for `map_config_to_obj`, so we need to mock that too.
+
+- [ ] **Step 1: Create test file**
+
+Create `tests/unit/modules/network/vyos/test_vyos_vlan.py`:
+
+```python
+# (c) 2016 Red Hat Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_vlan
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule
+
+
+SHOW_INTERFACES_OUTPUT = """\
+Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
+Interface IP Address S/L Description
+--------- ---------- --- -----------
+eth0 10.0.2.15/24 u/u
+eth0.100 - u/u vlan-100
+eth1 - u/u
+eth1.200 192.0.2.1/24 u/u vlan-200
+eth2 - u/u
+lo 127.0.0.1/8 u/u
+ ::1/128
+"""
+
+
+class TestVyosVlanModule(TestVyosModule):
+ module = vyos_vlan
+
+ def setUp(self):
+ super(TestVyosVlanModule, self).setUp()
+
+ self.mock_load_config = patch(
+ "ansible_collections.vyos.vyos.plugins.modules.vyos_vlan.load_config",
+ )
+ self.load_config = self.mock_load_config.start()
+
+ self.mock_run_commands = patch(
+ "ansible_collections.vyos.vyos.plugins.modules.vyos_vlan.run_commands",
+ )
+ self.run_commands = self.mock_run_commands.start()
+
+ def tearDown(self):
+ super(TestVyosVlanModule, self).tearDown()
+ self.mock_load_config.stop()
+ self.mock_run_commands.stop()
+
+ def load_fixtures(self, commands=None, filename=None):
+ self.load_config.return_value = dict(diff=None, session="session")
+ self.run_commands.return_value = [SHOW_INTERFACES_OUTPUT]
+
+ def test_vyos_vlan_present(self):
+ set_module_args(
+ dict(
+ vlan_id=300,
+ name="vlan-300",
+ interfaces=["eth2"],
+ state="present",
+ ),
+ )
+ commands = [
+ "set interfaces ethernet eth2 vif 300 description vlan-300",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_vlan_present_no_change(self):
+ set_module_args(
+ dict(
+ vlan_id=100,
+ interfaces=["eth0"],
+ state="present",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_vlan_absent(self):
+ set_module_args(
+ dict(
+ vlan_id=100,
+ interfaces=["eth0"],
+ state="absent",
+ ),
+ )
+ commands = [
+ "delete interfaces ethernet eth0 vif 100",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_vlan_absent_no_change(self):
+ set_module_args(
+ dict(
+ vlan_id=999,
+ interfaces=["eth0"],
+ state="absent",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+```
+
+- [ ] **Step 2: Run the tests**
+
+```bash
+pytest tests/unit/modules/network/vyos/test_vyos_vlan.py -vvv
+```
+
+Expected: All tests pass. The `show interfaces` mock output must match what `map_config_to_obj` parses — if tests fail, examine the parsing logic in `vyos_vlan.py:271-300` and adjust `SHOW_INTERFACES_OUTPUT` to produce the expected parsed objects.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add tests/unit/modules/network/vyos/test_vyos_vlan.py
+git commit -m "test: add unit tests for vyos_vlan module"
+```
+
+### Task 10: Verify full test suite and add changelog
+
+**Files:**
+- Create: `changelogs/fragments/missing-unit-tests.yml`
+
+- [ ] **Step 1: Run full test suite**
+
+```bash
+pytest tests/unit -vvv -n 2
+```
+
+Expected: All tests pass, including the 3 new test files. No regressions.
+
+- [ ] **Step 2: Create changelog fragment**
+
+Create `changelogs/fragments/missing-unit-tests.yml`:
+
+```yaml
+---
+minor_changes:
+ - Add unit tests for vyos_l3_interfaces, vyos_lldp_interfaces, and vyos_vlan modules.
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add changelogs/fragments/missing-unit-tests.yml
+git commit -m "chore: add changelog fragment for new unit tests"
+```
+
+---
+
+## Phase 5: Template Deduplication
+
+### Task 11: Merge identical route_maps templates
+
+**Files:**
+- Modify: `plugins/module_utils/network/vyos/rm_templates/route_maps_14.py`
+- Modify: `plugins/module_utils/network/vyos/config/route_maps/route_maps.py`
+- Modify: `plugins/module_utils/network/vyos/facts/route_maps/route_maps.py`
+
+The `route_maps.py` and `route_maps_14.py` templates are completely identical (1405 lines each) except for the class name (`Route_mapsTemplate` vs `Route_mapsTemplate14`). The `_14` file can be replaced with a thin subclass.
+
+- [ ] **Step 1: Create branch**
+
+```bash
+git checkout -b refactor/template-deduplication main
+```
+
+- [ ] **Step 2: Read current route_maps_14.py to confirm it's identical**
+
+```bash
+diff <(sed 's/Route_mapsTemplate14/Route_mapsTemplate/g; s/route_maps_14/route_maps/g' plugins/module_utils/network/vyos/rm_templates/route_maps_14.py) plugins/module_utils/network/vyos/rm_templates/route_maps.py
+```
+
+Expected: No differences (or only whitespace/comment differences). If there ARE functional differences, do not proceed with this step — investigate the differences first.
+
+- [ ] **Step 3: Replace route_maps_14.py with thin subclass**
+
+Replace the entire content of `plugins/module_utils/network/vyos/rm_templates/route_maps_14.py` with:
+
+```python
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+Route_maps 1.4+ template — identical to base, kept as alias for version dispatch.
+"""
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps import (
+ Route_mapsTemplate,
+)
+
+
+class Route_mapsTemplate14(Route_mapsTemplate):
+ pass
+```
+
+- [ ] **Step 4: Run route_maps tests**
+
+```bash
+pytest tests/unit/modules/network/vyos/test_vyos_route_maps.py tests/unit/modules/network/vyos/test_vyos_route_maps14.py -vvv
+```
+
+Expected: All tests pass.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add plugins/module_utils/network/vyos/rm_templates/route_maps_14.py
+git commit -m "refactor: replace route_maps_14 template with thin subclass of route_maps"
+```
+
+### Task 12: Deduplicate bgp_global templates
+
+**Files:**
+- Modify: `plugins/module_utils/network/vyos/rm_templates/bgp_global.py`
+- Modify: `plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py`
+
+The difference between these files is that pre-1.4 includes `{as_number}` in command path strings (e.g., `"protocols bgp {as_number} parameters confederation"`) while 1.4+ omits it (`"protocols bgp parameters confederation"`).
+
+Strategy: Add a `_bgp_prefix()` method to the base template class. Pre-1.4 returns `"protocols bgp {as_number}"`, 1.4+ returns `"protocols bgp"`. Template functions call this method instead of hardcoding the prefix. The `_14` file becomes a subclass overriding only `_bgp_prefix()`.
+
+**This task requires careful analysis.** Before implementing:
+
+- [ ] **Step 1: Identify all differing lines between the two files**
+
+```bash
+diff plugins/module_utils/network/vyos/rm_templates/bgp_global.py plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py | head -100
+```
+
+Examine the output. Every difference should be the `{as_number}` pattern in format strings. If there are other differences, document them and decide whether they can also be parameterized.
+
+- [ ] **Step 2: Count the differing template functions**
+
+```bash
+diff plugins/module_utils/network/vyos/rm_templates/bgp_global.py plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py | grep "^[<>]" | grep -c "protocols bgp"
+```
+
+This tells you how many command strings need to use the prefix method.
+
+- [ ] **Step 3: Add `_bgp_prefix` method to base class**
+
+In `plugins/module_utils/network/vyos/rm_templates/bgp_global.py`, find the class definition and add:
+
+```python
+class Bgp_globalTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(Bgp_globalTemplate, self).__init__(lines=lines, tmplt=self, prefix=prefix, module=module)
+
+ @staticmethod
+ def _bgp_prefix():
+ return "protocols bgp {as_number}"
+```
+
+- [ ] **Step 4: Convert template functions to use `_bgp_prefix()`**
+
+For each template function that contains a hardcoded `"protocols bgp {as_number}"` string, replace it with a call to `Bgp_globalTemplate._bgp_prefix()`. For example, change:
+
+```python
+def _tmplt_bgp_params_confederation(config_data):
+ command.append(
+ "protocols bgp {as_number} parameters confederation ".format(**config_data)
+ + k + " " + str(v),
+ )
+```
+
+to:
+
+```python
+def _tmplt_bgp_params_confederation(config_data):
+ command.append(
+ Bgp_globalTemplate._bgp_prefix() + " parameters confederation ".format(**config_data)
+ + k + " " + str(v),
+ )
+```
+
+**Important:** The `.format(**config_data)` call must still be applied to the entire string. Ensure the concatenation produces the same final string.
+
+**Alternative approach if template functions are module-level (not class methods):** Since template functions are standalone functions (not methods), they can't easily call instance methods. A simpler approach is to define a module-level constant:
+
+```python
+_BGP_PREFIX = "protocols bgp {as_number}"
+```
+
+Then in `bgp_global_14.py`:
+
+```python
+_BGP_PREFIX = "protocols bgp"
+```
+
+And update all template functions to use `_BGP_PREFIX` instead of the hardcoded string.
+
+- [ ] **Step 5: Replace bgp_global_14.py with thin override**
+
+Replace `plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py` with a file that:
+1. Imports the base template
+2. Overrides `_BGP_PREFIX = "protocols bgp"`
+3. Copies the PARSERS list (PARSERS reference template functions, so if the functions use module-level `_BGP_PREFIX`, each module needs its own copy of the functions that reference the correct prefix)
+
+**Caution:** If the template functions are module-level and reference a module-level `_BGP_PREFIX`, you cannot simply inherit — each file needs its own function definitions that reference its own `_BGP_PREFIX`. In this case, deduplication may not reduce total lines significantly. Evaluate whether the reduction is worth the added indirection.
+
+If deduplication doesn't produce significant reduction (less than 30% line reduction), **skip this subtask** and document why in the commit message.
+
+- [ ] **Step 6: Run bgp_global tests**
+
+```bash
+pytest tests/unit/modules/network/vyos/test_vyos_bgp_global.py tests/unit/modules/network/vyos/test_vyos_bgp_global14.py -vvv
+```
+
+Expected: All tests pass.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add plugins/module_utils/network/vyos/rm_templates/bgp_global.py plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py
+git commit -m "refactor: deduplicate bgp_global templates using shared prefix constant"
+```
+
+### Task 13: Deduplicate bgp_address_family templates
+
+**Files:**
+- Modify: `plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py`
+- Modify: `plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py`
+
+Same pattern as Task 12. The difference is `{as_number}` in command path strings.
+
+- [ ] **Step 1: Identify all differences**
+
+```bash
+diff plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py | head -100
+```
+
+- [ ] **Step 2: Apply the same deduplication strategy as Task 12**
+
+If Task 12 succeeded with the `_BGP_PREFIX` approach, apply the identical pattern here. If Task 12 concluded deduplication wasn't worth it, skip this task for the same reason.
+
+- [ ] **Step 3: Run tests**
+
+```bash
+pytest tests/unit/modules/network/vyos/test_vyos_bgp_address_family.py tests/unit/modules/network/vyos/test_vyos_bgp_address_family14.py -vvv
+```
+
+Expected: All tests pass.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py
+git commit -m "refactor: deduplicate bgp_address_family templates using shared prefix constant"
+```
+
+### Task 14: Assess ospf_interfaces deduplication
+
+**Files:**
+- Read: `plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py`
+- Read: `plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py`
+
+The OSPF templates have a structural difference: pre-1.4 uses interface-centric paths (`interfaces ethernet eth0 ip ospf`) while 1.4+ uses protocol-centric paths (`protocols ospf interface eth0`). This is fundamentally different from the BGP prefix pattern.
+
+- [ ] **Step 1: Measure shared vs divergent code**
+
+```bash
+diff plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py | grep "^[<>]" | wc -l
+```
+
+Compare against total lines:
+
+```bash
+wc -l plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py
+```
+
+If >30% of lines differ, **do not deduplicate** — the two templates represent genuinely different configuration paradigms. Document the decision.
+
+- [ ] **Step 2: If shared >70%, extract base (unlikely)**
+
+Only proceed if analysis shows >70% shared code. Otherwise, commit a decision note:
+
+```bash
+git commit --allow-empty -m "docs: ospf_interfaces templates not deduplicated — structural differences too large (interface-centric vs protocol-centric paths)"
+```
+
+### Task 15: Final verification and changelog
+
+**Files:**
+- Create: `changelogs/fragments/template-deduplication.yml`
+
+- [ ] **Step 1: Run full test suite**
+
+```bash
+pytest tests/unit -vvv -n 2
+```
+
+Expected: All tests pass. Zero regressions.
+
+- [ ] **Step 2: Create changelog fragment**
+
+Create `changelogs/fragments/template-deduplication.yml`:
+
+```yaml
+---
+minor_changes:
+ - Refactor version-specific rm_templates to reduce duplication between pre-1.4 and 1.4+ variants.
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add changelogs/fragments/template-deduplication.yml
+git commit -m "chore: add changelog fragment for template deduplication"
+```