From 1bee3f7d8e1dca104c0b5c5d7a7833946aa7514a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:38:28 -0700 Subject: Bump codecov/codecov-action from 5 to 6 (#452) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5...v6) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- .github/workflows/codecoverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml index 878ae247..8ce3af36 100644 --- a/.github/workflows/codecoverage.yml +++ b/.github/workflows/codecoverage.yml @@ -62,7 +62,7 @@ jobs: working-directory: ${{ steps.identify.outputs.collection_path }} - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: directory: ${{ steps.identify.outputs.collection_path }} fail_ci_if_error: false -- cgit v1.2.3 From db645c3fc5a66f0fdadd03b5096937f70787e3b0 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 17 Apr 2026 15:22:01 +0300 Subject: T8523: add Copilot custom review instructions (#465) * Add easy-wins improvement spec for vyos.vyos collection Covers five phases: formatting compliance, runtime.yml redirect fix, deprecated feature cleanup, missing unit tests, and template deduplication. Co-Authored-By: Claude Opus 4.6 (1M context) * Add implementation plan for easy-wins improvements 15 tasks across 5 phases: formatting compliance, runtime.yml bugfix, deprecated code cleanup, missing unit tests, template deduplication. Co-Authored-By: Claude Opus 4.6 (1M context) * Update spec and plan with architect review corrections Key changes: - Defer Phase 5 (template deduplication) to v7.0.0: route_maps are not identical, BGP dedup blocked by Python module-level scoping, OSPF has fundamentally different command paradigms - Add .git-blame-ignore-revs step to Phase 1 - Add missing test cases: overridden/rendered for resource modules, aggregate/purge/with_address for vyos_vlan - Fix incorrect claim that version.py LooseVersion is unused - Add sequential merge requirement to preamble Co-Authored-By: Claude Opus 4.6 (1M context) * chore: add .worktrees/ and .claude/ to .gitignore * T8523: add Copilot custom review instructions Add .github/copilot-instructions.md and path-specific instruction files under .github/instructions/ to teach Copilot project conventions: copyright attribution, changelog fragment key selection, test mocking patterns, required state coverage for resource modules, and module option description standards. Co-Authored-By: Claude Sonnet 4.6 * T8523: remove copyright instructions from Copilot config Copyright attribution rules are internal guidance kept in Claude config, not in Copilot review instructions. Co-Authored-By: Claude Sonnet 4.6 * T8523: remove docs/superpowers files inadvertently included These spec/plan files came from local main commits that were pulled in during rebase. They are unrelated to this PR. Co-Authored-By: Claude Sonnet 4.6 * T8523: fix inaccuracies flagged by Copilot review - CLI quoting: clarify that boolean flags and bare keywords are unquoted; only string/address values use single quotes - Test mocking: describe get_device_data / execute_show_command as the primary resource module pattern (used by most existing tests); note get_resource_connection_facts approach as an alternative - modules.instructions.md: broaden applyTo to include meta/runtime.yml so redirect guidance fires when that file is edited Co-Authored-By: Claude Sonnet 4.6 * T8523: fix legacy module mocking guidance in tests.instructions.md Existing legacy tests use side_effect for run_commands, not return_value. Updated to describe side_effect as the standard approach, note return_value as also acceptable for fixed responses, and clarify that the patched helper varies by module (run_commands for command/facts/ping, load_config/get_config for vyos_config). Co-Authored-By: Claude Sonnet 4.6 * T8523: fix four more inaccuracies flagged by Copilot review - CLI quoting: address values in firewall groups are unquoted in fixtures; instruct reviewers to align with surrounding fixture style rather than flagging all unquoted addresses - Resource module mocking: add .start()/.stop() calls to the snippet so it matches the actual setUp/tearDown pattern in existing tests - terminal plugin versions: reference README.md instead of hardcoding a version list that diverges from README - changelog fragment: remove stale "copyright headers" claim (that section was dropped from the instruction files) Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .github/copilot-instructions.md | 49 +++++++++++ .github/instructions/modules.instructions.md | 37 +++++++++ .github/instructions/tests.instructions.md | 114 ++++++++++++++++++++++++++ .gitignore | 6 ++ changelogs/fragments/copilot-instructions.yml | 4 + 5 files changed, 210 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/modules.instructions.md create mode 100644 .github/instructions/tests.instructions.md create mode 100644 changelogs/fragments/copilot-instructions.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..fc25faa1 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,49 @@ +# Copilot Review Instructions — vyos.vyos + +This is the `vyos.vyos` Ansible network collection for managing VyOS devices. +Namespace `vyos`, name `vyos`, version `6.0.0`. All modules are prefixed `vyos_`. + +## Commit and PR standards + +- Every commit title must start with a Phorge task ID: `T: description`. +- Every PR must have exactly one changelog fragment in `changelogs/fragments/`. +- PR descriptions that state a test count (e.g. "Add 8 unit tests") must match the actual number of test methods in the changed files. Flag mismatches. + +## Changelog fragments + +Fragments are YAML files under `changelogs/fragments/`. Valid top-level keys: + +| Key | Use for | +|-----|---------| +| `trivial` | Developer tooling, CI, housekeeping, formatting-only changes | +| `bugfixes` | Bug fixes | +| `minor_changes` | New features or user-visible improvements | +| `major_changes` | Breaking changes | +| `security_fixes` | Security fixes | +| `doc_changes` | Documentation-only changes | + +Flag any fragment that uses `minor_changes` for what is actually developer tooling (linting, formatting, gitignore, test scaffolding). Those should use `trivial`. + +## Module architecture + +Two module families: + +**Resource modules** (`vyos_interfaces`, `vyos_firewall_rules`, `vyos_bgp_global`, etc.) follow a four-part structure under `plugins/module_utils/network/vyos/`: +- `argspec/{resource}/` — argument spec +- `config/{resource}/` — config builder +- `facts/{resource}/` — facts parser +- `rm_templates/{resource}.py` — regex/Jinja2 CLI templates + +Resource modules support all states: `merged`, `replaced`, `overridden`, `deleted`, `rendered`, `gathered`, `parsed`. + +**Legacy modules** (`vyos_vlan`, `vyos_config`, `vyos_command`, `vyos_user`, etc.) do not follow the resource module pattern. + +## VyOS CLI conventions + +- Set commands: `set interfaces ethernet eth0 address '192.0.2.1/24'` +- Delete commands: `delete interfaces ethernet eth0 address '192.0.2.1/24'` +- Quoting varies by context. In general, string values (descriptions, names, ELIN numbers) are single-quoted; boolean flags and bare keywords are not. However, address/prefix values may be quoted or unquoted depending on where they appear: + - Quoted: `address '192.0.2.1/24'`, `description 'my-iface'`, `elin '0000000911'` + - Unquoted: `address 192.0.2.1` (in firewall groups), `disable`, `mtu-ignore`, `vif 200` +- When reviewing tests and fixtures, align with the quoting style used by surrounding fixtures rather than flagging a missing quote as an error. +- Interface types: `ethernet`, `loopback`, `bonding`, `bridge`, `tunnel`, `wireguard`. diff --git a/.github/instructions/modules.instructions.md b/.github/instructions/modules.instructions.md new file mode 100644 index 00000000..d7782e1e --- /dev/null +++ b/.github/instructions/modules.instructions.md @@ -0,0 +1,37 @@ +--- +applyTo: "plugins/**,meta/runtime.yml" +--- + +# Plugin conventions + +## Module option descriptions + +- Must be complete English sentences ending with a period. +- No grammar errors. Common mistake: "the number hops" should be "the number of hops". +- Deprecated options must include `removed_in_version` and `removed_from_collection` fields. +- Do not add new options with `deprecated: true` — remove deprecated options entirely. + +## rm_templates files + +Files in `plugins/module_utils/network/vyos/rm_templates/` define regex parsers and Jinja2 generators for a resource module. Each entry has: +- `name` — unique identifier +- `getval` — compiled regex with named groups +- `setval` — Jinja2 template or callable producing a VyOS CLI command +- `result` — dict mapping regex groups to facts structure +- `shared` (optional) — bool, whether the template applies to a shared config block + +## meta/runtime.yml redirects + +Short-name redirects must point to the actual module name with the `vyos_` prefix. For example: +```yaml +snmp_server: + redirect: vyos.vyos.vyos_snmp_server # correct + # NOT: vyos.vyos.vyos_snmp_servers # wrong (pluralized) +``` + +Verify any changed redirect target exists as a real module file under `plugins/modules/`. + +## cliconf / terminal plugins + +`plugins/cliconf/vyos.py` — do not modify without understanding edit-mode and commit semantics. +`plugins/terminal/vyos.py` — handles prompt detection; regex changes require testing against all supported VyOS versions listed in README.md. diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md new file mode 100644 index 00000000..150db41e --- /dev/null +++ b/.github/instructions/tests.instructions.md @@ -0,0 +1,114 @@ +--- +applyTo: "tests/unit/**" +--- + +# Unit test conventions + +## Base class and structure + +All test classes inherit from `TestVyosModule` in `tests/unit/modules/network/vyos/vyos_module.py`. + +```python +class TestVyosFooModule(TestVyosModule): + module = vyos_foo + + def setUp(self): ... + def tearDown(self): ... + def load_fixtures(self, commands=None, filename=None): ... + def test_...(self): ... +``` + +## Mocking: resource modules + +Resource module tests require two framework-level patches in `setUp` with corresponding cleanup in `tearDown`: + +```python +# in setUp: +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() + +# in tearDown: +self.mock_get_resource_connection_config.stop() +self.mock_get_resource_connection_facts.stop() +``` + +Most resource module tests also patch the facts class's `get_device_data` method directly and use it in `load_fixtures`: + +```python +# in setUp: +self.mock_execute_show_command = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos." + "facts.{resource}.{resource}.{Resource}Facts.get_device_data" +) +self.execute_show_command = self.mock_execute_show_command.start() + +# in load_fixtures: +def load_from_file(*args, **kwargs): + return load_fixture("vyos_{resource}_config.cfg") +self.execute_show_command.side_effect = load_from_file +``` + +An alternative pattern sets `self.get_resource_connection_facts.return_value.get_config.return_value = fixture_data` in `load_fixtures` instead — both approaches work, but the `get_device_data` pattern is used by most existing tests. + +## Mocking: legacy modules + +Legacy modules (`vyos_vlan`, `vyos_config`, `vyos_command`, etc.) patch the specific helper the module calls, directly in the module under test. Which helper depends on the module: + +```python +# vyos_command, vyos_facts, vyos_ping — patch run_commands +self.mock_run_commands = patch("ansible_collections.vyos.vyos.plugins.modules.vyos_foo.run_commands") +# vyos_config — patch get_config / load_config +self.mock_load_config = patch("ansible_collections.vyos.vyos.plugins.modules.vyos_foo.load_config") +``` + +In `load_fixtures`, configure the mock using `side_effect` (for dynamic fixture loading) or `return_value` (for a fixed response): + +```python +# side_effect — used by most existing legacy tests +def load_from_file(*args, **kwargs): + return load_fixture("vyos_foo_config.cfg") +self.run_commands.side_effect = load_from_file + +# return_value — acceptable for simple fixed responses +self.run_commands.return_value = [SHOW_OUTPUT] +``` + +## load_fixtures and filename + +`execute_module()` always calls `load_fixtures()`. Never set mock return values inside a test method — they will be overwritten by the next `execute_module` call. Use the `filename` parameter to vary the fixture: + +```python +def load_fixtures(self, commands=None, filename=None): + if filename == "empty": + self.run_commands.return_value = [EMPTY_OUTPUT] + else: + self.run_commands.return_value = [DEFAULT_OUTPUT] +``` + +Then call: `self.execute_module(changed=True, commands=commands, filename="empty")` + +## Fixture files + +Raw device CLI output lives in `tests/unit/modules/network/vyos/fixtures/` as `.cfg` files. +Load with `load_fixture("vyos_foo_config.cfg")`. + +## Required test coverage for resource modules + +A complete resource module test file should cover all applicable states: +- `merged` (including an idempotent case) +- `replaced` +- `overridden` +- `deleted` +- `rendered` — assert `result["rendered"]` matches expected CLI commands +- `gathered` — assert `result["gathered"]` contains expected structured data +- `parsed` — pass `running_config=raw_string` and assert `result["parsed"]` + +Flag test files that are missing `rendered`, `gathered`, or `parsed` tests without explanation. + diff --git a/.gitignore b/.gitignore index cbe347c4..1dbe8b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,9 @@ changelogs/.plugin-cache.yaml inventory.network *.bak + +# Git worktrees +.worktrees/ + +# Claude Code +.claude/ diff --git a/changelogs/fragments/copilot-instructions.yml b/changelogs/fragments/copilot-instructions.yml new file mode 100644 index 00000000..c7af5c74 --- /dev/null +++ b/changelogs/fragments/copilot-instructions.yml @@ -0,0 +1,4 @@ +--- +trivial: + - Add Copilot custom review instructions to guide automated code review with + project-specific conventions for tests, changelog fragments, and module patterns. -- cgit v1.2.3 From 24814ecbe107e2aa9198d0a96361c7c5b0b0378c Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sun, 26 Apr 2026 17:11:07 +0300 Subject: T8584: add CodeRabbit review configuration (#467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * T8584: add CodeRabbit review configuration Add .coderabbit.yaml with path-specific guidelines for automated code review, docstring generation, and unit test generation covering all module types (resource and legacy), plugin layers, and test infrastructure. 🤖 Generated by [robots](https://vyos.io) * T8584: fix changelog glob, valid keys, and vyos_vrf classification - Expand changelog fragment glob to *.{yaml,yml} (18 of 19 fragments use .yml) - Add known_issues and release_summary to valid changelog keys per changelogs/config.yaml - Remove vyos_vrf from legacy module list — it has resource module infrastructure (argspec, config, facts) 🤖 Generated by [robots](https://vyos.io) * T8584: add ansible/ansible and ansible.netcommon as linked repositories CodeRabbit will reference core Ansible framework and netcommon base classes when reviewing this collection modules, plugins, and test patterns. 🤖 Generated by [robots](https://vyos.io) --- .coderabbit.yaml | 591 +++++++++++++++++++++++++++++ changelogs/fragments/coderabbit-config.yml | 3 + 2 files changed, 594 insertions(+) create mode 100644 .coderabbit.yaml create mode 100644 changelogs/fragments/coderabbit-config.yml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..38eae56f --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,591 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# CodeRabbit configuration for vyos/vyos.vyos Ansible network collection +# Docs: https://docs.coderabbit.ai/guides/configure-coderabbit + +language: en-US +early_access: false +tone_instructions: > + Concise, technical, no filler. Focus on correctness, security, idempotency, + and Ansible conventions. Cite file paths and line numbers. + +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + high_level_summary_placeholder: '@coderabbitai summary' + auto_title_placeholder: '@coderabbitai' + review_status: true + poem: false + collapse_walkthrough: true + changed_files_summary: true + sequence_diagrams: false + assess_linked_issues: true + related_issues: true + related_prs: true + suggested_labels: false + auto_apply_labels: false + suggested_reviewers: false + + auto_review: + enabled: true + auto_incremental_review: true + drafts: false + base_branches: + - main + ignore_title_keywords: + - WIP + - DO NOT MERGE + - Bump + + path_filters: + - '!**/__pycache__/**' + - '!**/*.pyc' + - '!**/*.egg-info/**' + - '!changelogs/changelog.yaml' + - '!.venv/**' + - '!.collections/**' + - '!.worktrees/**' + + path_instructions: + # ── Global PR hygiene ────────────────────────────────────────────── + - path: '**' + instructions: | + This is the vyos.vyos Ansible network collection (namespace=vyos, name=vyos, + version=6.0.0). PR titles must follow the format `T{id}: description` referencing + a Phorge task at vyos.dev. Every PR must include a changelog fragment in + changelogs/fragments/ (YAML, valid keys: major_changes, minor_changes, + breaking_changes, deprecated_features, removed_features, security_fixes, + bugfixes, known_issues, doc_changes, trivial; plus release_summary as a + prelude section). Style: black line-length=100, isort + profile=black line_length=100, flake8 max-line-length=120. Do not suggest + 88-char wrapping. + + # ── Module entry points ──────────────────────────────────────────── + - path: 'plugins/modules/vyos_*.py' + instructions: | + Module entry points. Each file must contain three YAML triple-string blocks: + DOCUMENTATION, EXAMPLES, and RETURN — this is Ansible's documentation contract, + not Python docstrings. Verify: + - DOCUMENTATION includes: module, author, short_description, description, + version_added, extends_documentation_fragment (vyos.vyos.vyos), options with + types and descriptions, and a notes section listing tested VyOS versions. + - EXAMPLES has at least one working task per supported state. + - RETURN documents all return keys with description, returned, type, and sample. + - The module wires argspec, config, and facts classes correctly. + - State choices include the full set where applicable: merged, replaced, + overridden, deleted, gathered, parsed, rendered. + Do not add Python-style docstrings (def-level) to these files — the YAML blocks + are the canonical documentation. + + # ── Argspec (auto-generated) ─────────────────────────────────────── + - path: 'plugins/module_utils/network/vyos/argspec/**' + instructions: | + Auto-generated by the Ansible resource module builder. These files carry a + "DO NOT EDIT" warning header. Do not suggest modifications to auto-generated + argspec files — changes will be overwritten. If the schema needs updating, + the resource module builder must regenerate it. Only flag issues if the + argument_spec dict has obvious type mismatches or missing required fields + that would cause runtime failures. + + # ── Config classes ───────────────────────────────────────────────── + - path: 'plugins/module_utils/network/vyos/config/**' + instructions: | + Config builders extending ansible.netcommon ConfigBase or ResourceModule. + These generate VyOS CLI commands from desired state. Verify: + - execute_module() handles all declared states correctly. + - set_config() and _set_config() process gathered facts and desired config + without data loss. + - Command generation produces valid VyOS CLI syntax (set/delete prefixes, + proper quoting of values with spaces). + - No silent swallowing of unknown keys — unknown config should raise or warn. + - Methods that compare current vs desired state handle empty/None gracefully. + Some older config files have auto-generated headers — do not restructure those. + + # ── Facts classes ────────────────────────────────────────────────── + - path: 'plugins/module_utils/network/vyos/facts/**' + instructions: | + Facts classes parse raw VyOS CLI output into structured dicts. Verify: + - Regex patterns handle edge cases (missing fields, empty values, quoted strings). + - populate() returns a clean dict even when device output is incomplete. + - get_device_data() uses the correct show command for the resource. + - facts/facts.py FACT_RESOURCE_SUBSETS and FACT_LEGACY_SUBSETS stay in sync + with available fact classes. + - Legacy facts (facts/legacy/) use run_commands(); resource facts use + get_resource_connection(). + + # ── RM Templates ─────────────────────────────────────────────────── + - path: 'plugins/module_utils/network/vyos/rm_templates/*.py' + instructions: | + Parser templates mapping structured data to VyOS CLI commands and vice versa. + Files with a `_14` suffix target VyOS 1.4+ behavior — do not suggest merging + them with the base version. Verify: + - _tmplt_* helper functions produce syntactically valid VyOS commands. + - Regex patterns in PARSERS list correctly capture all variations of the CLI + output (quoted values, optional fields, nested hierarchies). + - New templates include both set and delete command generation. + - compval/getval paths match the argspec structure. + + # ── Cliconf plugin ───────────────────────────────────────────────── + - path: 'plugins/cliconf/vyos.py' + instructions: | + Low-level CLI abstraction for VyOS. Handles configure mode, commit, diff, + command execution. Changes here affect all modules. Verify: + - edit_config() enters configure mode and commits correctly. + - get_diff() returns accurate before/after config diffs. + - Error handling catches VyOS-specific error patterns (commit failures, + invalid commands). + - __rpc__ list matches actually implemented methods. + + # ── Terminal plugin ──────────────────────────────────────────────── + - path: 'plugins/terminal/vyos.py' + instructions: | + Terminal prompt detection and initialization. Changes affect connection + reliability. Verify regex patterns against actual VyOS prompt formats + (configure mode, operational mode, different shell variants). Do not + remove existing patterns without testing against all supported VyOS versions. + + # ── Action plugin ────────────────────────────────────────────────── + - path: 'plugins/action/vyos.py' + instructions: | + Auto-proxies all modules to the device. Must validate network_cli connection + type. Symlinks from each module name point here. Keep minimal — logic belongs + in config classes, not the action plugin. + + # ── Changelog fragments ──────────────────────────────────────────── + - path: 'changelogs/fragments/*.{yaml,yml}' + instructions: | + Changelog fragments for ansible-changelog. Valid top-level keys: + major_changes, minor_changes, breaking_changes, deprecated_features, + removed_features, security_fixes, bugfixes, known_issues, doc_changes, + trivial. release_summary is a prelude section (one per release). + Fragment filename should be descriptive (e.g., fix-bgp-neighbor-timers.yml). + Use `trivial` for tooling/housekeeping. Entries should be complete sentences. + + # ── CI workflows ─────────────────────────────────────────────────── + - path: '.github/workflows/**' + instructions: | + CI pipeline: tests.yml (main CI with changelog, build, lint, sanity, unit jobs), + codecoverage.yml, release.yml (Galaxy + Automation Hub publish), check_label.yaml, + cla-check.yml. Changes to release.yml or ah_token_refresh.yml affect publishing + credentials — review with extra care. Do not remove the `all_green` aggregation + job from tests.yml. + + # ── Unit tests ───────────────────────────────────────────────────── + - path: 'tests/unit/**' + instructions: | + Unit tests use pytest + unittest.TestCase via TestVyosModule base class. + Key patterns: + - All test classes inherit TestVyosModule (from vyos_module.py). + - setUp() creates and starts mock patches; tearDown() stops them. + - execute_module(failed, changed, commands, sort) is the primary assertion method. + - load_fixtures() is overridden per test class to wire mock return values. + - Fixture files (.cfg) go in tests/unit/modules/network/vyos/fixtures/. + - Use load_fixture(name) to read fixtures — never inline raw config strings. + - set_module_args(dict(...)) configures module input before execution. + Style: black line-length=100, assertions via self.assertEqual / self.assertIn / + execute_module kwargs. pytest-xdist runs tests in parallel (-n 2). + + # ── Test fixtures ────────────────────────────────────────────────── + - path: 'tests/unit/modules/network/vyos/fixtures/**' + instructions: | + Raw VyOS CLI output files (.cfg). These are loaded by load_fixture() and + cached in memory. Format is VyOS `set ...` configuration syntax or show + command output. Fixture filenames follow the pattern: + vyos_{module}_config.cfg (base) or vyos_{module}_config_v14.cfg (VyOS 1.4+). + New fixtures must be syntactically valid VyOS config. Do not add JSON fixtures + unless the test explicitly requires JSON parsing. + + # ── Collection metadata ──────────────────────────────────────────── + - path: 'galaxy.yml' + instructions: | + Collection metadata. namespace=vyos, name=vyos. Version bumps must be + coordinated with release process. Dependency on ansible.netcommon>=2.5.1 + is required. Do not add unnecessary dependencies. + + - path: 'meta/runtime.yml' + instructions: | + Module redirects and tombstones. Adding a new module requires a redirect + entry (short name → FQCN). Tombstoned modules (logging, vyos_logging) must + not be un-tombstoned. requires_ansible must stay >=2.15.0 unless explicitly + bumping minimum version. + + finishing_touches: + docstrings: + enabled: true + unit_tests: + enabled: true + + tools: + github-checks: + enabled: true + timeout_ms: 90000 + eslint: + enabled: false + biome: + enabled: false + actionlint: + enabled: true + yamllint: + enabled: true + markdownlint: + enabled: true + languagetool: + enabled: true + level: default + enabled_only: false + gitleaks: + enabled: true + checkov: + enabled: false + semgrep: + enabled: true + ast-grep: + essential_rules: true + ruff: + enabled: false + +chat: + auto_reply: true + +knowledge_base: + opt_out: false + learnings: + scope: auto + issues: + scope: auto + pull_requests: + scope: auto + linked_repositories: + - repository: "ansible/ansible" + instructions: > + Core Ansible framework. Reference for module_utils base classes, + plugin interfaces (cliconf, terminal, action), module documentation + conventions (DOCUMENTATION/EXAMPLES/RETURN YAML blocks), and + ansible-test sanity requirements. + - repository: "ansible-collections/ansible.netcommon" + instructions: > + Network common collection. Contains ConfigBase, ResourceModule, + FactsBase, NetworkTemplate, and get_resource_connection — the base + classes and utilities that vyos.vyos modules directly extend. + +code_generation: + docstrings: + language: en-US + path_instructions: + # ── Module entry points: YAML blocks, not Python docstrings ────── + - path: 'plugins/modules/vyos_*.py' + instructions: | + Do NOT generate Python-style docstrings for these files. Ansible modules + use YAML triple-string blocks: DOCUMENTATION, EXAMPLES, and RETURN. + If updating these blocks: + - DOCUMENTATION must include: module name, author, short_description, + description (list of strings), version_added, extends_documentation_fragment + (vyos.vyos.vyos), and a full options tree with type, description, and + choices/default where applicable. Include a notes section listing supported + VyOS versions (1.3.8, 1.4.1, 1.4.2, 1.5 rolling). + - EXAMPLES must show at least one task per supported state using FQCN + (vyos.vyos.vyos_). + - RETURN must document: commands (list, always), before (dict, always), + after (dict, when changed), and any module-specific return values. + Keep version_added accurate — do not backdate. + + # ── Argspec: skip auto-generated files ─────────────────────────── + - path: 'plugins/module_utils/network/vyos/argspec/**' + instructions: | + Skip — these files are auto-generated by the Ansible resource module builder + and carry a "DO NOT EDIT" header. Do not generate or modify docstrings. + + # ── Config classes ─────────────────────────────────────────────── + - path: 'plugins/module_utils/network/vyos/config/**' + instructions: | + Config builder classes extending ConfigBase or ResourceModule. Use + reStructuredText-style docstrings (Ansible/Sphinx convention): + def method(self, ...): + """Short description. + + :param name: description + :type name: type + :rtype: type + :returns: description + """ + Document: execute_module(), set_config(), get__facts(), and any + method that generates CLI commands. Focus on what state transitions the + method handles and what CLI commands it may produce. Do not document trivial + __init__ that just calls super(). Some files have auto-generated headers — + keep docstrings minimal in those to avoid noise on regeneration. + + # ── Facts classes ──────────────────────────────────────────────── + - path: 'plugins/module_utils/network/vyos/facts/**' + instructions: | + Facts parsers that convert VyOS CLI output to structured dicts. Use rST + docstrings. Document: + - populate(): what show commands it runs and the dict structure it returns. + - render_config() / get_device_data(): the CLI command used and expected + output format. + - Any regex-heavy parsing method: briefly note what CLI patterns it handles. + Skip __init__.py files. + + # ── RM Templates ───────────────────────────────────────────────── + - path: 'plugins/module_utils/network/vyos/rm_templates/*.py' + instructions: | + Parser template files with _tmplt_* helper functions and PARSERS lists. + Add a module-level docstring describing the resource and VyOS CLI hierarchy + covered. For _tmplt_* functions: one-line docstring stating the VyOS command + path generated (e.g., "Generate `set protocols bgp neighbor + timers ...` commands."). Do not document individual regex PARSERS entries — + the patterns are self-describing. Files with _14 suffix target VyOS 1.4+ — + note this in the module docstring. + + # ── Cliconf plugin ────────────────────────────────────────────── + - path: 'plugins/cliconf/vyos.py' + instructions: | + Uses Ansible DOCUMENTATION block for plugin-level docs. For Python methods + use rST docstrings. Document: get_device_info(), edit_config(), get_config(), + get_diff(), commit(), discard_changes(). Note which methods enter/exit + configure mode. + + # ── Terminal and action plugins ────────────────────────────────── + - path: 'plugins/terminal/vyos.py' + instructions: | + Minimal docstrings only. Document on_open_shell() and on_close_shell() + explaining what terminal parameters they set. Regex patterns are + self-documenting — do not add inline comments to each pattern. + + - path: 'plugins/action/vyos.py' + instructions: | + Skip — thin proxy inheriting ActionNetworkModule. No docstrings needed. + + # ── Utilities ──────────────────────────────────────────────────── + - path: 'plugins/module_utils/network/vyos/utils/*.py' + instructions: | + Utility functions shared across modules. Use rST docstrings for any + function with non-obvious behavior. Document parameters, return types, + and side effects. Skip trivial one-liners. + + # ── Test files ────────────────────────────────────────────────── + - path: 'tests/**' + instructions: | + Skip — test files do not need docstrings. Test method names should be + self-describing (test_vyos___). + + unit_tests: + path_instructions: + # ── Resource module tests ──────────────────────────────────────── + - path: 'plugins/modules/vyos_interfaces.py' + instructions: &resource_module_tests | + Generate tests in tests/unit/modules/network/vyos/test_vyos_.py. + Inherit from TestVyosModule (import from vyos_module.py in same directory). + + Structure: + ```python + class TestVyosModule(TestVyosModule): + module = vyos_ + + def setUp(self): + super().setUp() + # Mock get_resource_connection at BOTH levels: + self.mock_get_resource_connection_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils." + "network.common.cfg.base.get_resource_connection" + ) + self.mock_get_resource_connection_facts = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils." + "network.common.facts.facts.get_resource_connection" + ) + # Mock the facts get_device_data method: + self.mock_execute_show_command = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network." + "vyos.facts....get_device_data" + ) + # Start all patches and store references + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super().tearDown() + # Stop ALL patches + + def load_fixtures(self, commands=None, filename=None): + def load_from_file(*args, **kwargs): + return load_fixture(filename or "vyos__config.cfg") + self.execute_show_command.side_effect = load_from_file + ``` + + Required test methods for each resource module: + - test_vyos__merged: config change, changed=True, verify commands list + - test_vyos__merged_idempotent: no-op, changed=False, commands=[] + - test_vyos__replaced: replaced state, changed=True + - test_vyos__replaced_idempotent: replaced no-op, changed=False + - test_vyos__overridden: full override, changed=True + - test_vyos__deleted: deletion, changed=True + - test_vyos__gathered: state=gathered, verify result["gathered"] dict + - test_vyos__rendered: state=rendered, verify result["rendered"] commands + - test_vyos__parsed: state=parsed with running_config, verify output + + Assertions use self.execute_module(changed=True/False, commands=[...]). + Commands lists contain exact VyOS CLI strings: "set interfaces ethernet eth0 ...". + Use set_module_args(dict(config=[...], state="")) before execute_module. + Create fixture files in tests/unit/modules/network/vyos/fixtures/ named + vyos__config.cfg with valid VyOS set-syntax configuration. + Use load_fixture() to read fixtures — never inline raw config. + + - path: 'plugins/modules/vyos_l3_interfaces.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_lag_interfaces.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_lldp_global.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_lldp_interfaces.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_static_routes.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_firewall_rules.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_firewall_global.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_firewall_interfaces.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_ospfv2.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_ospfv3.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_ospf_interfaces.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_bgp_global.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_bgp_address_family.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_prefix_lists.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_route_maps.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_snmp_server.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_logging_global.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_ntp_global.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_hostname.py' + instructions: *resource_module_tests + + - path: 'plugins/modules/vyos_vrf.py' + instructions: *resource_module_tests + + # ── Legacy module tests ────────────────────────────────────────── + - path: 'plugins/modules/vyos_command.py' + instructions: &legacy_module_tests | + Generate tests in tests/unit/modules/network/vyos/test_vyos_.py. + Inherit from TestVyosModule. + + Legacy modules mock differently from resource modules: + ```python + class TestVyosModule(TestVyosModule): + module = vyos_ + + def setUp(self): + super().setUp() + # Mock run_commands directly on the module: + self.mock_run_commands = patch( + "ansible_collections.vyos.vyos.plugins.modules." + "vyos_.run_commands" + ) + self.run_commands = self.mock_run_commands.start() + # Some also mock get_capabilities or get_config/load_config + + def tearDown(self): + super().tearDown() + self.mock_run_commands.stop() + + def load_fixtures(self, commands=None, filename=None): + # Set run_commands return_value or side_effect + self.run_commands.return_value = [load_fixture(filename)] + ``` + + Legacy modules (vyos_command, vyos_config, vyos_facts, vyos_banner, + vyos_ping, vyos_system, vyos_user, vyos_vlan) do not use + resource module states. Test: successful execution, error handling, + idempotency where applicable, and specific module features (e.g., + vyos_command wait_for/retries, vyos_config src/lines/match). + Use execute_module(changed=, commands=) for assertions. + + - path: 'plugins/modules/vyos_config.py' + instructions: *legacy_module_tests + + - path: 'plugins/modules/vyos_facts.py' + instructions: *legacy_module_tests + + - path: 'plugins/modules/vyos_banner.py' + instructions: *legacy_module_tests + + - path: 'plugins/modules/vyos_ping.py' + instructions: *legacy_module_tests + + - path: 'plugins/modules/vyos_system.py' + instructions: *legacy_module_tests + + - path: 'plugins/modules/vyos_user.py' + instructions: *legacy_module_tests + + - path: 'plugins/modules/vyos_vlan.py' + instructions: *legacy_module_tests + + # ── Test infrastructure — do not generate tests for these ──────── + - path: 'tests/unit/modules/utils.py' + instructions: | + Skip — test infrastructure (ModuleTestCase base, set_module_args, exception + classes). Do not generate tests for test utilities. + + - path: 'tests/unit/modules/conftest.py' + instructions: | + Skip — pytest fixtures (patch_ansible_module). Do not generate tests. + + - path: 'tests/unit/modules/network/vyos/vyos_module.py' + instructions: | + Skip — TestVyosModule base class with execute_module(), load_fixture(), + and mock setup. Do not generate tests for the test base class. + + - path: 'tests/unit/modules/network/vyos/fixtures/**' + instructions: | + Skip — raw VyOS CLI output fixtures. Not code, not testable. + + # ── Non-module plugin code ─────────────────────────────────────── + - path: 'plugins/module_utils/**' + instructions: | + Module utility code (argspec, config, facts, rm_templates, utils). + These are tested indirectly through module-level tests — the config + classes are exercised when test_vyos_.py calls execute_module(). + Do not generate separate unit tests for module_utils classes unless + a utility function in plugins/module_utils/network/vyos/utils/ has + complex standalone logic worth testing in isolation. + + - path: 'plugins/cliconf/vyos.py' + instructions: | + Skip — cliconf plugin is tested via integration tests and indirectly + through module tests. Unit testing requires complex CliconfBase mocking + that provides little value over integration coverage. + + - path: 'plugins/terminal/vyos.py' + instructions: | + Skip — terminal plugin regex patterns are validated through integration + tests against actual VyOS devices. + + - path: 'plugins/action/vyos.py' + instructions: | + Skip — thin action proxy. Tested indirectly via module tests. diff --git a/changelogs/fragments/coderabbit-config.yml b/changelogs/fragments/coderabbit-config.yml new file mode 100644 index 00000000..9442337f --- /dev/null +++ b/changelogs/fragments/coderabbit-config.yml @@ -0,0 +1,3 @@ +trivial: + - Add CodeRabbit review configuration with path-specific guidelines for review, + docstring generation, and unit test generation. -- cgit v1.2.3 From 80035465ca7345e9ba5aae8e6d49cf536bd75aa0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:19:48 +0300 Subject: Bump ansible/team-devtools/.github/workflows/ah_token_refresh.yml (#448) Bumps [ansible/team-devtools/.github/workflows/ah_token_refresh.yml](https://github.com/ansible/team-devtools) from 26.1.0 to 26.2.0. - [Release notes](https://github.com/ansible/team-devtools/releases) - [Commits](https://github.com/ansible/team-devtools/compare/v26.1.0...v26.2.0) --- updated-dependencies: - dependency-name: ansible/team-devtools/.github/workflows/ah_token_refresh.yml dependency-version: 26.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Daniil Baturin Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> Co-authored-by: Yuriy Andamasov --- .github/workflows/ah_token_refresh.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ah_token_refresh.yml b/.github/workflows/ah_token_refresh.yml index 0346920e..0c8cba7b 100644 --- a/.github/workflows/ah_token_refresh.yml +++ b/.github/workflows/ah_token_refresh.yml @@ -7,7 +7,7 @@ on: jobs: refresh: - uses: ansible/team-devtools/.github/workflows/ah_token_refresh.yml@v26.1.0 + uses: ansible/team-devtools/.github/workflows/ah_token_refresh.yml@v26.2.0 with: environment: release secrets: -- cgit v1.2.3 From d3f981148e78c0dd31d35063f3407530278aa3a1 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Mon, 27 Apr 2026 16:47:01 +0300 Subject: T8512: fix isort import ordering violations (#454) * T8512: fix isort import ordering violations * T8512: add changelog fragment Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- changelogs/fragments/T8512-isort-fix.yml | 3 +++ .../network/vyos/facts/bgp_address_family/bgp_address_family.py | 6 +++--- plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py | 7 +++---- .../module_utils/network/vyos/rm_templates/ospf_interfaces_14.py | 1 + plugins/module_utils/network/vyos/utils/version.py | 2 ++ 5 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 changelogs/fragments/T8512-isort-fix.yml diff --git a/changelogs/fragments/T8512-isort-fix.yml b/changelogs/fragments/T8512-isort-fix.yml new file mode 100644 index 00000000..5209017c --- /dev/null +++ b/changelogs/fragments/T8512-isort-fix.yml @@ -0,0 +1,3 @@ +--- +trivial: + - Fix isort import ordering violations across module_utils to satisfy pre-commit checks. diff --git a/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py b/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py index 3386bd66..31839c5d 100644 --- a/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py +++ b/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py @@ -28,11 +28,11 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_template from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_address_family_14 import ( Bgp_address_familyTemplate14, ) - +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Bgp_address_familyFacts(object): """The vyos bgp_address_family facts class""" diff --git a/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py b/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py index dd793681..2883cc2d 100644 --- a/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py +++ b/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py @@ -25,15 +25,14 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.bgp from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_global import ( Bgp_globalTemplate, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_global_14 import ( Bgp_globalTemplate14, ) - +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Bgp_globalFacts(object): """The vyos bgp_global facts class""" diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py index 43fae1e9..0d3aa5a7 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function + __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/utils/version.py b/plugins/module_utils/network/vyos/utils/version.py index cc3028c3..6d84ef1c 100644 --- a/plugins/module_utils/network/vyos/utils/version.py +++ b/plugins/module_utils/network/vyos/utils/version.py @@ -7,6 +7,8 @@ """Provide version object to compare version numbers.""" from __future__ import absolute_import, division, print_function + + __metaclass__ = type -- cgit v1.2.3 From a2ee09579a75165ae691efcbf3e952db83145b83 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 1 May 2026 21:43:03 +0300 Subject: T8520: fix vlan purge generating invalid commands for bare interfaces (#463) Co-authored-by: Daniil Baturin --- changelogs/fragments/fix-vlan-purge.yml | 3 + plugins/modules/vyos_vlan.py | 12 +-- tests/unit/modules/network/vyos/test_vyos_vlan.py | 119 ++++++++++++++++++++++ 3 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 changelogs/fragments/fix-vlan-purge.yml create mode 100644 tests/unit/modules/network/vyos/test_vyos_vlan.py diff --git a/changelogs/fragments/fix-vlan-purge.yml b/changelogs/fragments/fix-vlan-purge.yml new file mode 100644 index 00000000..bbc1d08a --- /dev/null +++ b/changelogs/fragments/fix-vlan-purge.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - vyos_vlan - fix purge generating invalid ``delete ... vif None`` commands for bare interfaces without VLAN sub-interfaces. diff --git a/plugins/modules/vyos_vlan.py b/plugins/modules/vyos_vlan.py index f0b68bc9..9d23cc7c 100644 --- a/plugins/modules/vyos_vlan.py +++ b/plugins/modules/vyos_vlan.py @@ -279,15 +279,11 @@ def map_config_to_obj(module): obj = {} eth = splitted_line[0].strip("'") - if eth.startswith("eth"): + if eth.startswith("eth") and "." in eth: obj["interfaces"] = [] - if "." in eth: - interface = eth.split(".")[0] - obj["interfaces"].append(interface) - obj["vlan_id"] = eth.split(".")[-1] - else: - obj["interfaces"].append(eth) - obj["vlan_id"] = None + interface = eth.split(".")[0] + obj["interfaces"].append(interface) + obj["vlan_id"] = eth.split(".")[-1] if splitted_line[1].strip("'") != "-": obj["address"] = splitted_line[1].strip("'") diff --git a/tests/unit/modules/network/vyos/test_vyos_vlan.py b/tests/unit/modules/network/vyos/test_vyos_vlan.py new file mode 100644 index 00000000..4f2ea69a --- /dev/null +++ b/tests/unit/modules/network/vyos/test_vyos_vlan.py @@ -0,0 +1,119 @@ +# (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_purge_no_bare_interfaces(self): + """Purge should only delete VLANs, not bare interfaces without vlan_id.""" + set_module_args( + dict( + vlan_id=100, + interfaces=["eth0"], + state="present", + purge=True, + ), + ) + result = self.execute_module(changed=True) + # Should only delete eth1.200, not bare eth0/eth1/eth2 with vif None + for cmd in result.get("commands", []): + self.assertNotIn( + "vif None", + cmd, + "Purge generated 'vif None' command for bare interface: {0}".format(cmd), + ) + # eth1.200 should be purged since it's not in the desired state + self.assertIn( + "delete interfaces ethernet eth1 vif 200", + result.get("commands", []), + ) + + 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_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) -- cgit v1.2.3 From 29aac3de8d3b222ee3644ba1a192daaa765adbf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 16:45:46 -0700 Subject: Bump ansible/team-devtools/.github/workflows/ah_token_refresh.yml (#468) Bumps [ansible/team-devtools/.github/workflows/ah_token_refresh.yml](https://github.com/ansible/team-devtools) from 26.2.0 to 26.4.0. - [Release notes](https://github.com/ansible/team-devtools/releases) - [Commits](https://github.com/ansible/team-devtools/compare/v26.2.0...v26.4.0) --- updated-dependencies: - dependency-name: ansible/team-devtools/.github/workflows/ah_token_refresh.yml dependency-version: 26.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- .github/workflows/ah_token_refresh.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ah_token_refresh.yml b/.github/workflows/ah_token_refresh.yml index 0c8cba7b..09d2f9db 100644 --- a/.github/workflows/ah_token_refresh.yml +++ b/.github/workflows/ah_token_refresh.yml @@ -7,7 +7,7 @@ on: jobs: refresh: - uses: ansible/team-devtools/.github/workflows/ah_token_refresh.yml@v26.2.0 + uses: ansible/team-devtools/.github/workflows/ah_token_refresh.yml@v26.4.0 with: environment: release secrets: -- cgit v1.2.3 From f1e00c85fea01b805e0b4f86aed014ccf4fae00e Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 8 May 2026 21:27:40 +0300 Subject: general: T8595: add AGENTS.md (#472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * general: T8595: add AGENTS.md * general: T8595: clean leaked internal references * general: T8595: restore eaten spaces in shell command examples * general: T8595: correct CI scope (no integration in CI yet) and version-matrix attribution * general: T8595: add changelog fragment + refine CI scope (unit-galaxy/unit-source) * general: T8595: address Copilot review threads on test command and commit convention - Line 15: add `ansible-test units` alongside the existing pytest command; the two correspond to the unit-galaxy and unit-source CI jobs respectively, so documenting both gives contributors accurate options for each CI path. - Line 30: change "Commit / PR title" to "Commit headline". No workflow in this repo enforces PR title format; the PR template checklist confirms only commit headlines must carry a Phorge task ID. 🤖 Generated by [robots](https://vyos.io) --- .github/copilot-instructions.md | 50 +--------------------------- AGENTS.md | 39 ++++++++++++++++++++++ changelogs/fragments/T8595_add_agents_md.yml | 2 ++ 3 files changed, 42 insertions(+), 49 deletions(-) mode change 100644 => 120000 .github/copilot-instructions.md create mode 100644 AGENTS.md create mode 100644 changelogs/fragments/T8595_add_agents_md.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index fc25faa1..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,49 +0,0 @@ -# Copilot Review Instructions — vyos.vyos - -This is the `vyos.vyos` Ansible network collection for managing VyOS devices. -Namespace `vyos`, name `vyos`, version `6.0.0`. All modules are prefixed `vyos_`. - -## Commit and PR standards - -- Every commit title must start with a Phorge task ID: `T: description`. -- Every PR must have exactly one changelog fragment in `changelogs/fragments/`. -- PR descriptions that state a test count (e.g. "Add 8 unit tests") must match the actual number of test methods in the changed files. Flag mismatches. - -## Changelog fragments - -Fragments are YAML files under `changelogs/fragments/`. Valid top-level keys: - -| Key | Use for | -|-----|---------| -| `trivial` | Developer tooling, CI, housekeeping, formatting-only changes | -| `bugfixes` | Bug fixes | -| `minor_changes` | New features or user-visible improvements | -| `major_changes` | Breaking changes | -| `security_fixes` | Security fixes | -| `doc_changes` | Documentation-only changes | - -Flag any fragment that uses `minor_changes` for what is actually developer tooling (linting, formatting, gitignore, test scaffolding). Those should use `trivial`. - -## Module architecture - -Two module families: - -**Resource modules** (`vyos_interfaces`, `vyos_firewall_rules`, `vyos_bgp_global`, etc.) follow a four-part structure under `plugins/module_utils/network/vyos/`: -- `argspec/{resource}/` — argument spec -- `config/{resource}/` — config builder -- `facts/{resource}/` — facts parser -- `rm_templates/{resource}.py` — regex/Jinja2 CLI templates - -Resource modules support all states: `merged`, `replaced`, `overridden`, `deleted`, `rendered`, `gathered`, `parsed`. - -**Legacy modules** (`vyos_vlan`, `vyos_config`, `vyos_command`, `vyos_user`, etc.) do not follow the resource module pattern. - -## VyOS CLI conventions - -- Set commands: `set interfaces ethernet eth0 address '192.0.2.1/24'` -- Delete commands: `delete interfaces ethernet eth0 address '192.0.2.1/24'` -- Quoting varies by context. In general, string values (descriptions, names, ELIN numbers) are single-quoted; boolean flags and bare keywords are not. However, address/prefix values may be quoted or unquoted depending on where they appear: - - Quoted: `address '192.0.2.1/24'`, `description 'my-iface'`, `elin '0000000911'` - - Unquoted: `address 192.0.2.1` (in firewall groups), `disable`, `mtu-ignore`, `vif 200` -- When reviewing tests and fixtures, align with the quoting style used by surrounding fixtures rather than flagging a missing quote as an error. -- Interface types: `ethernet`, `loopback`, `bonding`, `bridge`, `tunnel`, `wireguard`. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..436b6785 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# AGENTS.md + +## Project purpose +The official Ansible Collection for managing VyOS network appliances (`vyos.vyos` namespace). Provides modules, plugins, action handlers, terminal plugins, and resource modules for BGP, OSPF, firewall, interfaces, NTP, etc. + +## Tech stack +- Ansible Collection (Galaxy). Python control-plane code under `plugins/`. +- `galaxy.yml` declares `namespace: vyos`, `name: vyos`, `version: 6.0.0`, dep `ansible.netcommon >= 2.5.1`, license_file `LICENSE` (GPL-3.0). +- Test stack: `pytest` + `tox-ansible.ini`; lint via flake8, isort, black (line-length 100), pre-commit, ansible-lint. +- Runtime deps: `paramiko`, `scp` (`requirements.txt`); `bindep.txt` for system deps. + +## Build / test / run +- Build: `ansible-galaxy collection build` produces a `vyos-vyos-.tar.gz`. +- Install local dev: `ansible-galaxy collection install . --force`. +- Test (unit): `ansible-test units` (matches `unit-galaxy` CI job; requires collection installed under `~/.ansible/collections/`). Fast local alternative: `source .venv/bin/activate && PYTHONPATH=".collections" python -m pytest tests/unit` (matches `unit-source` CI path; config in `pyproject.toml`). CI (`.github/workflows/tests.yml`) runs the changelog / build-import / ansible-lint / sanity / unit-galaxy / unit-source jobs; integration tests live under `tests/integration/` but are not yet wired into CI. Per `README.md`, the collection targets VyOS 1.3.8 / 1.4.1 / 1.5-rolling (no version matrix in the workflow itself). + +## Repository layout +- `plugins/{action,cliconf,doc_fragments,filter,inventory,module_utils,modules,terminal}/` — collection content. +- `tests/` — sanity, unit, integration directories. CI (`.github/workflows/tests.yml`) runs sanity + unit-galaxy + unit-source (plus changelog / build-import / ansible-lint); integration is not yet wired into CI. +- `docs/` — generated module docs. +- `meta/`, `changelogs/`, `CHANGELOG.rst` — Galaxy + release metadata. +- `pyproject.toml` (black/pytest config), `.flake8`, `.isort.cfg`, `.ansible-lint`, `.pre-commit-config.yaml`. +- `.github/workflows/` — `tests.yml`, `release.yml`, `codecoverage.yml`, `cla-check.yml`, `ah_token_refresh.yml`, `check_label.yaml`. + +## Cross-repo context +- Consumed by Ansible users running playbooks against VyOS routers built by `vyos/vyos-build`. +- The `vyos.vyos` collection talks to VyOS via `network_cli` connections; supports the same train branches (`current`, `circinus`, `sagitta`, `equuleus`). + +## Conventions +- Commit headline: `T12345: description` (Phorge ID at https://vyos.dev mandatory). No workflow enforces PR title format in this repo. +- Every PR must include exactly one changelog fragment under `changelogs/fragments/`; use `doc_changes` for documentation-only updates, or `trivial` for tooling / housekeeping changes. +- Default branch `main` (not `current` — this repo predates the rename convention). +- Issues tracked at https://vyos.dev (see `galaxy.yml`). +- Codecov + CodeRabbit configured (`codecov.yml`, `.coderabbit.yaml`). + +## Notes for future contributors +- Galaxy versioning is independent of VyOS train versioning — bump in `galaxy.yml` per release. +- Tested matrix is in README; expand only after smoketesting against real images. +- `PR408_README.md` plus `pr408-diagram.png` document a non-trivial historical refactor; read before touching resource-module structure. diff --git a/changelogs/fragments/T8595_add_agents_md.yml b/changelogs/fragments/T8595_add_agents_md.yml new file mode 100644 index 00000000..5d7fb0a4 --- /dev/null +++ b/changelogs/fragments/T8595_add_agents_md.yml @@ -0,0 +1,2 @@ +trivial: + - Add AGENTS.md tool-neutral contributor documentation, with .github/copilot-instructions.md symlink for Copilot code review compatibility. -- cgit v1.2.3 From be77e3b4b870ae2fb8ec4a75794a960c422a8449 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Wed, 13 May 2026 16:23:39 +0300 Subject: T8515: remove pre-1.3 deprecated code artifacts from vyos_bgp_global (#461) * T8515: remove commented-out pre-1.3 parameter artifacts from vyos_bgp_global docs * T8515: remove stale vrf.old backup file * T8515: add changelog fragment for deprecated code cleanup * T8515: fix grammar in ebgp_multihop description Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- changelogs/fragments/deprecated-cleanup.yml | 3 + .../module_utils/network/vyos/config/vrf/vrf.old | 313 --------------------- plugins/modules/vyos_bgp_global.py | 139 +-------- 3 files changed, 4 insertions(+), 451 deletions(-) create mode 100644 changelogs/fragments/deprecated-cleanup.yml delete mode 100644 plugins/module_utils/network/vyos/config/vrf/vrf.old diff --git a/changelogs/fragments/deprecated-cleanup.yml b/changelogs/fragments/deprecated-cleanup.yml new file mode 100644 index 00000000..cf361adc --- /dev/null +++ b/changelogs/fragments/deprecated-cleanup.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - vyos_bgp_global - remove commented-out pre-1.3 deprecated parameter documentation artifacts. diff --git a/plugins/module_utils/network/vyos/config/vrf/vrf.old b/plugins/module_utils/network/vyos/config/vrf/vrf.old deleted file mode 100644 index b9f56cc6..00000000 --- a/plugins/module_utils/network/vyos/config/vrf/vrf.old +++ /dev/null @@ -1,313 +0,0 @@ -# -# -*- coding: utf-8 -*- -# Copyright 2021 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 - -""" -The vyos_vrf config file. -It is in this file where the current configuration (as dict) -is compared to the provided configuration (as dict) and the command set -necessary to bring the current configuration to its desired end-state is -created. -""" - -import importlib - -from copy import deepcopy - -from ansible.module_utils.six import iteritems -from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( - ResourceModule, -) - -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_global.bgp_global import ( - Bgp_global, -) -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vrf import ( - VrfTemplate, -) -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( - LooseVersion, -) -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version - - -# from ansible.plugins.filter.core import combine - - -class Vrf(ResourceModule): - """ - The vyos_vrf config class - """ - - def __init__(self, module): - super(Vrf, self).__init__( - empty_fact_val={}, - facts_module=Facts(module), - module=module, - resource="vrf", - tmplt=VrfTemplate(), - ) - self.parsers = [ - "bind_to_all", - ] - self.bgp = Bgp_global(module) - - def _validate_template(self): - version = get_os_version(self._module) - if LooseVersion(version) >= LooseVersion("1.4"): - self._tmplt = VrfTemplate() - else: - self._module.fail_json(msg="VRF is not supported in this version of VyOS") - - def parse(self): - """override parse to check template""" - self._validate_template() - return super().parse() - - def get_parser(self, name): - """get_parsers""" - self._validate_template() - return super().get_parser(name) - - def execute_module(self): - """Execute the module - - :rtype: A dictionary - :returns: The result from module execution - """ - if self.state not in ["parsed", "gathered"]: - self.generate_commands() - self.run_commands() - - return self.result - - def generate_commands(self): - """Generate configuration commands to send based on - want, have and desired state. - """ - wantd = {} - haved = {} - wantd = deepcopy(self.want) - haved = deepcopy(self.have) - - # self._module.fail_json(msg="WanT: " + str(self.want) + "**** H: " + str(self.have)) - - # if state is merged, merge want onto have and then compare - if self.state in ["merged", "replaced"]: - # wantd = dict_merge(wantd, haved) - # wantd = haved | combine(wantd, recursive=True) - wantd = combine(haved, wantd, recursive=True) - # self._module.fail_json(msg="Want: " + str(wantd) + "**** H: " + str(haved)) - - # if state is deleted, delete and empty out wantd - if self.state == "deleted": - w = deepcopy(wantd) - if w == {} and haved != {}: - self.commands = ["delete vrf"] - return - for k, want in iteritems(w): - if not (k in haved and haved[k]): - del wantd[k] - else: - if isinstance(want, list): - for entry in want: - wname = entry.get("name") - haved["instances"] = [ - i for i in haved.get("instances", []) if i.get("name") != wname - ] - self.commands.append("delete vrf name {}".format(wname)) - else: - self.commands.append("delete vrf {}".format(k.replace("_", "-"))) - del wantd[k] - - if self.state == "overridden": - w = deepcopy(wantd) - h = deepcopy(haved) - for k, want in iteritems(w): - if k in haved and haved[k] != want: - if isinstance(want, list): - for entry in want: - wname = entry.get("name") - hdict = next( - (inst for inst in haved["instances"] if inst["name"] == wname), - None, - ) - if entry != hdict: - # self._module.fail_json(msg="Want: " + str(entry) + "**** H: " + str(hdict)) - haved["instances"] = [ - i for i in haved.get("instances", []) if i.get("name") != wname - ] - self.commands.append("delete vrf name {}".format(wname)) - self.commands.append("commit") - - for k, want in iteritems(wantd): - if isinstance(want, list): - self._compare_instances(want=want, have=haved.pop(k, {})) - self.compare( - parsers=self.parsers, - want={k: want}, - have={k: haved.pop(k, {})}, - ) - self._module.fail_json(msg=self.commands) - - def _compare_instances(self, want, have): - """Compare the instances of the VRF""" - parsers = [ - "table_id", - "vni", - "description", - "disable_vrf", - ] - # self._module.fail_json(msg="want: " + str(want) + "**** have: " + str(have)) - - for entry in want: - h = {} - wname = entry.get("name") - # h = next((vrf for vrf in have if vrf["name"] == wname), {}) - h = { - k: v - for vrf in have - if vrf.get("name") == wname - for k, v in vrf.items() - if k != "address_family" - } - self.compare(parsers=parsers, want=entry, have=h) - - if "address_family" in entry: - wafi = {"name": wname, "address_family": entry.get("address_family", [])} - # hdict = next((item for item in have if item["name"] == wname), None) - hdict = next((d for d in have if d.get("name") == wname), None) - - hafi = { - "name": (hdict or {"name": wname})["name"], - "address_family": hdict.get("address_family", []) if hdict else [], - } - - # self._module.fail_json(msg="wafi: " + str(wafi) + "**** hafi: " + str(hafi)) - - self._compare_addr_family(wafi, hafi) - - if "protocols" in entry: - for protocol_name in entry["protocols"]: - protocol_module = self._load_protocol_module(protocol_name) - w_p_dict = entry["protocols"][protocol_name] - h_p_dict = next( - ( - v.get("protocols", {}).get(protocol_name) - for v in have - if v.get("name") == wname - ), - {}, - ) - if protocol_name == "bgp": - protocol_module._validate_template() - protocol_module.want = w_p_dict - protocol_module.have = h_p_dict - protocol_module.generate_commands() - elif protocol_name in [ - # "ospf", - # "ospfv3", - "static", - ]: - self._module.fail_json(msg=str(protocol_module)) - protocol_module._module.params["config"] = w_p_dict - protocol_module.state = self.state - self._module.fail_json(msg=str(protocol_module.set_config(h_p_dict))) - - protocol_module.commands = protocol_module.set_config(h_p_dict) - self.commands.extend( - [ - cmd.replace("protocols", "vrf name " + wname + " protocols", 1) - for cmd in protocol_module.commands - ], - ) - protocol_module = None # Clear the module to free resources - - def _compare_addr_family(self, want, have): - """Compare the address families of the VRF""" - afi_parsers = [ - # "address_family", - "disable_forwarding", - "disable_nht", - ] - # self._module.fail_json(msg="wAfi: " + str(want) + "**** hAfi: " + str(have)) - - wafi = self.afi_to_list(want) - hafi = self.afi_to_list(have) - - lookup = {(d["name"], d["afi"]): d for d in hafi} - pairs = [(d1, lookup.get((d1["name"], d1["afi"]), {})) for d1 in wafi] - - for wafd, hafd in pairs: - # self._module.fail_json(msg="wAfd: " + str(wafd) + "**** hAfd: " + str(hafd)) - if "route_maps" in wafd: - self._compare_route_maps(wafd, hafd) - self.compare(parsers=afi_parsers, want=wafd, have=hafd) - # self.compare(parsers=afi_parsers, want=wafi, have=hafi) - - def afi_to_list(self, data): - """Convert address family dict to list""" - - return [ - {"name": data["name"], **{**af, "afi": "ip" if af["afi"] == "ipv4" else af["afi"]}} - for af in data["address_family"] - ] - - def _compare_route_maps(self, wafd, hafd): - want_rms = wafd.get("route_maps", []) - have_rms = hafd.get("route_maps", []) - - for want in want_rms: - match = next( - ( - h - for h in have_rms - if h["rm_name"] == want["rm_name"] and h["protocol"] == want["protocol"] - ), - {}, - ) - base = {"name": wafd["name"], "afi": wafd["afi"]} - - self.compare( - parsers="route_maps", - want={**base, "route_maps": want}, - have={**base, "route_maps": match}, - ) - - def _load_protocol_module(self, protocol_name): - if protocol_name == "bgp": - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_global.bgp_global import ( - Bgp_global, - ) - - return Bgp_global(self._module) - elif protocol_name == "ospf": - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv2.ospfv2 import ( - Ospfv2, - ) - - return Ospfv2(self._module) - elif protocol_name == "ospfv3": - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv3.ospfv3 import ( - Ospfv3, - ) - - return Ospfv3(self._module) - elif protocol_name == "static": - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.static_routes.static_routes import ( - Static_routes, - ) - - return Static_routes(self._module) - else: - self._module.fail_json(msg="The protocol is not supported") diff --git a/plugins/modules/vyos_bgp_global.py b/plugins/modules/vyos_bgp_global.py index fe14bc0f..7e9f63b1 100644 --- a/plugins/modules/vyos_bgp_global.py +++ b/plugins/modules/vyos_bgp_global.py @@ -74,37 +74,6 @@ options: description: - Minimum interval for sending routing updates. type: int - # bfd: # <-- added in 1.3 - # description: Enable Bidirectional Forwarding Detection (BFD) support - # type: dict - # suboptions: - # check-control-plane-failure: - # description: - # - Allow to write CBIT independence in BFD outgoing packets - # and read both C-BIT value of BFD and lookup BGP peer status - # type: bool - # allowas_in: --> Moved to address-family before 1.3 - # description: - # - Number of occurrences of AS number. - # type: int - # as_override: --> Moved to address-family before 1.3 - # description: - # - AS for routes sent to this neighbor to be the local AS. - # type: bool - # attribute_unchanged: --> Moved to address-family before 1.3 - # description: - # - BGP attributes are sent unchanged. - # type: dict - # suboptions: - # as_path: - # description: as_path - # type: bool - # med: - # description: med - # type: bool - # next_hop: - # description: next_hop - # type: bool capability: description: - Advertise capabilities to this neighbor. @@ -118,13 +87,6 @@ options: description: - Advertise extended nexthop capability to this neighbor. type: bool - # orf: --> Removed before 1.3 - # description: - # - Advertise ORF capability to this neighbor. - # type: str - # choices: - # - send - # - receive default_originate: description: - Send default route to this neighbor @@ -146,70 +108,14 @@ options: - Disable sending community attributes to this neighbor. type: str choices: ['extended', 'standard'] - # distribute_list: --> Moved to address-family before 1.3 - # description: Access-list to filter route updates to/from this neighbor. - # type: list - # elements: dict - # suboptions: - # action: - # description: Access-list to filter outgoing/incoming route updates to this neighbor - # type: str - # choices: ['export', 'import'] - # acl: - # description: Access-list number. - # type: int ebgp_multihop: description: - Allow this EBGP neighbor to not be on a directly connected network. Specify - the number hops. + the number of hops. type: int - # interface: # <-- added in 1.3 - # description: interface parameters - # type: dict - # suboptions: - # peer_group: - # description: Peer group for this neighbor - # type: str - # remote_as: - # description: - # - Remote AS number - # - Or 'external' for any number except this AS number - # - or 'internal' for this AS number - # type: str - # v6only: - # description: Enable BGP with v6 link-local only - # type: dict - # suboptions: - # peer_group: - # description: Peer group for this neighbor - # type: str - # remote_as: - # description: - # - Remote AS number - # - Or 'external' for any number except this AS number - # - or 'internal' for this AS number - # filter_list: --> Moved to address-family before 1.3 - # description: As-path-list to filter route updates to/from this neighbor. - # type: list - # elements: dict - # suboptions: - # action: - # description: filter outgoing/incoming route updates - # type: str - # choices: ['export', 'import'] - # path_list: - # description: As-path-list to filter - # type: str local_as: description: local as number not to be prepended to updates from EBGP peers type: int - # maximum_prefix: --> Moved to address-family before 1.3 - # description: Maximum number of prefixes to accept from this neighbor - # nexthop-self Nexthop for routes sent to this neighbor to be the local router. - # type: int - # nexthop_self: --> Moved to address-family before 1.3 - # description: Nexthop for routes sent to this neighbor to be the local router. - # type: bool override_capability: description: Ignore capability negotiation with specified neighbor. type: bool @@ -228,61 +134,18 @@ options: port: description: Neighbor's BGP port type: int - # prefix_list: --> Moved to address-family before 1.3 - # description: Prefix-list to filter route updates to/from this neighbor. - # type: list - # elements: dict - # suboptions: - # action: - # description: filter outgoing/incoming route updates - # type: str - # choices: ['export', 'import'] - # prefix_list: - # description: Prefix-list to filter - # type: str remote_as: description: Neighbor BGP AS number type: int - # remove_private_as: --> Moved to address-family before 1.3 - # description: Remove private AS numbers from AS path in outbound route updates - # type: bool - # route_map: --> Moved to address-family before 1.3 - # description: Route-map to filter route updates to/from this neighbor. - # type: list - # elements: dict - # suboptions: - # action: - # description: filter outgoing/incoming route updates - # type: str - # choices: ['export', 'import'] - # route_map: - # description: route-map to filter - # type: str - # route_reflector_client: --> Moved to address-family before 1.3 - # description: Neighbor as a route reflector client - # type: bool - # route_server_client: --> Removed prior to 1.3 - # description: Neighbor is route server client - # type: bool shutdown: description: Administratively shut down neighbor type: bool - # soft_reconfiguration: --> Moved to address-family before 1.3 - # description: Soft reconfiguration for neighbor - # type: bool solo: # <-- added in 1.3 description: Do not send back prefixes learned from the neighbor type: bool strict_capability_match: description: Enable strict capability negotiation type: bool - # unsuppress_map: --> Moved to address-family before 1.3 - # description: Route-map to selectively unsuppress suppressed routes - # type: str - - # weight: --> Moved to address-family before 1.3 - # description: Default weight for routes from this neighbor - # type: int timers: description: Neighbor timers type: dict -- cgit v1.2.3 From e8acd05dd327bd4eb2419de1b931960feddec427 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 16 May 2026 00:34:13 +0300 Subject: T8518: add unit tests for vyos_vlan module (#471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 7 unit tests for vyos_vlan covering present, present_no_change, absent, absent_no_change, aggregate, purge, and address scenarios. Fixture data moved to dedicated .cfg files under fixtures/. 🤖 Generated by [robots](https://vyos.io) Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> Co-authored-by: Daniil Baturin --- changelogs/fragments/T8518-vlan-unit-tests.yml | 3 + .../vyos/fixtures/vyos_vlan_show_interfaces.cfg | 10 ++ .../fixtures/vyos_vlan_show_interfaces_empty.cfg | 7 ++ tests/unit/modules/network/vyos/test_vyos_vlan.py | 123 ++++++++++++++------- 4 files changed, 101 insertions(+), 42 deletions(-) create mode 100644 changelogs/fragments/T8518-vlan-unit-tests.yml create mode 100644 tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces.cfg create mode 100644 tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces_empty.cfg diff --git a/changelogs/fragments/T8518-vlan-unit-tests.yml b/changelogs/fragments/T8518-vlan-unit-tests.yml new file mode 100644 index 00000000..65fcdfd5 --- /dev/null +++ b/changelogs/fragments/T8518-vlan-unit-tests.yml @@ -0,0 +1,3 @@ +--- +trivial: + - Add unit tests for the vyos_vlan module covering present, absent, aggregate, purge, and address scenarios. diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces.cfg new file mode 100644 index 00000000..b877a4b6 --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces.cfg @@ -0,0 +1,10 @@ +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 diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces_empty.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces_empty.cfg new file mode 100644 index 00000000..06ae56a0 --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces_empty.cfg @@ -0,0 +1,7 @@ +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 +eth1 - u/u +eth2 - u/u +lo 127.0.0.1/8 u/u diff --git a/tests/unit/modules/network/vyos/test_vyos_vlan.py b/tests/unit/modules/network/vyos/test_vyos_vlan.py index 4f2ea69a..5bbf87c2 100644 --- a/tests/unit/modules/network/vyos/test_vyos_vlan.py +++ b/tests/unit/modules/network/vyos/test_vyos_vlan.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +# Make coding more python3-ish from __future__ import absolute_import, division, print_function @@ -25,21 +26,7 @@ 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 -""" +from .vyos_module import TestVyosModule, load_fixture class TestVyosVlanModule(TestVyosModule): @@ -65,55 +52,107 @@ class TestVyosVlanModule(TestVyosModule): 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] + if filename == "empty": + self.run_commands.return_value = [ + load_fixture("vyos_vlan_show_interfaces_empty.cfg"), + ] + else: + self.run_commands.return_value = [ + load_fixture("vyos_vlan_show_interfaces.cfg"), + ] + + def test_vyos_vlan_present(self): + """Create a new VLAN with a description on eth2 (not in have).""" + 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_purge_no_bare_interfaces(self): - """Purge should only delete VLANs, not bare interfaces without vlan_id.""" + def test_vyos_vlan_present_no_change(self): + """Existing VLAN 100 on eth0 — no commands should be generated.""" set_module_args( dict( vlan_id=100, + name="vlan-100", interfaces=["eth0"], state="present", - purge=True, - ), + ) ) - result = self.execute_module(changed=True) - # Should only delete eth1.200, not bare eth0/eth1/eth2 with vif None - for cmd in result.get("commands", []): - self.assertNotIn( - "vif None", - cmd, - "Purge generated 'vif None' command for bare interface: {0}".format(cmd), + self.execute_module(changed=False, commands=[]) + + def test_vyos_vlan_absent(self): + """Delete an existing VLAN (200 on eth1).""" + set_module_args( + dict( + vlan_id=200, + interfaces=["eth1"], + state="absent", ) - # eth1.200 should be purged since it's not in the desired state - self.assertIn( - "delete interfaces ethernet eth1 vif 200", - result.get("commands", []), ) + commands = ["delete interfaces ethernet eth1 vif 200"] + self.execute_module(changed=True, commands=commands) - def test_vyos_vlan_present(self): + def test_vyos_vlan_absent_no_change(self): + """Delete a VLAN that does not exist — no commands.""" set_module_args( dict( - vlan_id=300, - name="vlan-300", - interfaces=["eth2"], - state="present", - ), + vlan_id=999, + interfaces=["eth0"], + state="absent", + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_vlan_aggregate(self): + """Create two new VLANs via aggregate; neither is in have.""" + set_module_args( + dict( + aggregate=[ + dict(vlan_id=301, interfaces=["eth2"], name="vlan-301"), + dict(vlan_id=302, interfaces=["eth2"], name="vlan-302"), + ], + ) ) commands = [ - "set interfaces ethernet eth2 vif 300 description vlan-300", + "set interfaces ethernet eth2 vif 301 description vlan-301", + "set interfaces ethernet eth2 vif 302 description vlan-302", ] self.execute_module(changed=True, commands=commands) - def test_vyos_vlan_absent(self): + def test_vyos_vlan_purge(self): + """Purge VLANs not in want. Want only VLAN 100; VLAN 200 should be removed. + + The fixed parser only maps ethX.Y sub-interfaces, so bare ethX interfaces + (vlan_id=None) are not in have. Only real VLANs (eth1.200) are purged. + """ set_module_args( dict( vlan_id=100, interfaces=["eth0"], - state="absent", - ), + state="present", + purge=True, + ) ) commands = [ - "delete interfaces ethernet eth0 vif 100", + "delete interfaces ethernet eth1 vif 200", ] self.execute_module(changed=True, commands=commands) + + def test_vyos_vlan_with_address(self): + """Create a VLAN with an IP address and no description.""" + set_module_args( + dict( + vlan_id=400, + address="10.10.40.1/24", + interfaces=["eth1"], + state="present", + ) + ) + commands = ["set interfaces ethernet eth1 vif 400 address 10.10.40.1/24"] + self.execute_module(changed=True, commands=commands, filename="empty") -- cgit v1.2.3 From 29e47bb4b43d1888c79f54de2fbee90224e9652b Mon Sep 17 00:00:00 2001 From: stronkbyte <262002943+stronkbyte@users.noreply.github.com> Date: Mon, 25 May 2026 13:44:54 +0200 Subject: T6818: Add argument to vyos_config for controlling password filtering (#466) This commit adds the argument `allow_password_change` in order to control whether any configuration lines which would make changes to user passwords should be filtered out or not. Co-authored-by: Daniil Baturin Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- changelogs/fragments/t6818_password_filtering.yml | 2 + plugins/modules/vyos_config.py | 47 +++++++++++++++++----- .../vyos_config/tests/cli/check_config.yaml | 34 ++++++++++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 changelogs/fragments/t6818_password_filtering.yml diff --git a/changelogs/fragments/t6818_password_filtering.yml b/changelogs/fragments/t6818_password_filtering.yml new file mode 100644 index 00000000..d52283b9 --- /dev/null +++ b/changelogs/fragments/t6818_password_filtering.yml @@ -0,0 +1,2 @@ +minor_changes: + - plugins/modules/vyos_config.py - Added an argument to control password filtering in vyos_config. Current filtering behavior is still the default. diff --git a/plugins/modules/vyos_config.py b/plugins/modules/vyos_config.py index a9774638..2407ce01 100644 --- a/plugins/modules/vyos_config.py +++ b/plugins/modules/vyos_config.py @@ -142,6 +142,20 @@ options: in C(filename) within I(backup) directory. type: path type: dict + allow_password_change: + description: + - The C(allow_password_change) argument specifies whether any configuration lines which + would change a user's password should be filtered out. By default only plaintext + password changes are allowed and any encrypted-password keys are filtered out. In + order to allow all password updates, both plaintext and encrypted, set this argument + to C(all). + type: str + default: plaintext + choices: + - all + - plaintext + - encrypted + - none """ EXAMPLES = """ @@ -233,9 +247,7 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import DEFAULT_COMMENT = "configured by vyos_config" -CONFIG_FILTERS = [ - re.compile(r"set system login user \S+ authentication encrypted-password"), -] +PASSWORD_NEEDLE = re.compile(r"set system login user \S+ authentication (encrypted|plaintext)-password") def get_candidate(module): @@ -294,14 +306,26 @@ def diff_config(commands, config): return list(updates) -def sanitize_config(config, result): +def sanitize_config(config, result, allow): result["filtered"] = list() + + if allow == "all": + return + index_to_filter = list() - for regex in CONFIG_FILTERS: - for index, line in enumerate(list(config)): - if regex.search(line): - result["filtered"].append(line) - index_to_filter.append(index) + + for index, line in enumerate(list(config)): + found = PASSWORD_NEEDLE.search(line) + + if found is None: + continue + + if allow == found[1]: + continue + + result["filtered"].append(line) + index_to_filter.append(index) + # Delete all filtered configs for filter_index in sorted(index_to_filter, reverse=True): del config[filter_index] @@ -328,7 +352,9 @@ def run(module, result): module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) commands = response.get("config_diff") - sanitize_config(commands, result) + + allow_password_change = module.params["allow_password_change"] + sanitize_config(commands, result, allow=allow_password_change) result["commands"] = commands @@ -368,6 +394,7 @@ def main(): backup=dict(type="bool", default=False), backup_options=dict(type="dict", options=backup_spec), save=dict(type="bool", default=False), + allow_password_change=dict(default="plaintext", choices=["all", "encrypted", "plaintext", "none"]) ) mutually_exclusive = [("lines", "src")] diff --git a/tests/integration/targets/vyos_config/tests/cli/check_config.yaml b/tests/integration/targets/vyos_config/tests/cli/check_config.yaml index 8e2e8372..83a62197 100644 --- a/tests/integration/targets/vyos_config/tests/cli/check_config.yaml +++ b/tests/integration/targets/vyos_config/tests/cli/check_config.yaml @@ -49,9 +49,43 @@ - set system login user esa authentication encrypted-password '!abc!' - set system login user vyos full-name 'VyOS admin' - set system login user vyos authentication encrypted-password 'abc' + - set system login user john full-name 'John' + - set system login user john authentication plaintext-password 'xyz' - assert: that: - result.filtered|length == 2 +- name: check multiple line config filter is working + register: result + vyos.vyos.vyos_config: + allow_password_change: none + lines: + - set system login user esa full-name 'ESA admin' + - set system login user esa authentication encrypted-password '!abc!' + - set system login user vyos full-name 'VyOS admin' + - set system login user vyos authentication encrypted-password 'abc' + - set system login user john full-name 'John' + - set system login user john authentication plaintext-password 'xyz' + +- assert: + that: + - result.filtered|length == 3 + +- name: check multiple line config filter is working + register: result + vyos.vyos.vyos_config: + allow_password_change: all + lines: + - set system login user esa full-name 'ESA admin' + - set system login user esa authentication encrypted-password '!abc!' + - set system login user vyos full-name 'VyOS admin' + - set system login user vyos authentication encrypted-password 'abc' + - set system login user john full-name 'John' + - set system login user john authentication plaintext-password 'xyz' + +- assert: + that: + - result.filtered|length == 0 + - debug: msg="END cli/config_check.yaml on connection={{ ansible_connection }}" -- cgit v1.2.3 From 70bb194f16ca719e7991ff2edb700240442b9de9 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Mon, 25 May 2026 21:00:20 +0300 Subject: coderabbit: T8851: add .coderabbit.yaml for central-config inheritance (#475) * ci: switch .coderabbit.yaml to centralized inheritance (T8851) * ci: T8851: add changelog fragment for CodeRabbit centralization --------- Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- .coderabbit.yaml | 444 ++------------------- .../fragments/t8851-coderabbit-centralized.yml | 6 + 2 files changed, 35 insertions(+), 415 deletions(-) create mode 100644 changelogs/fragments/t8851-coderabbit-centralized.yml diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 38eae56f..c14e52eb 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,50 +1,35 @@ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json -# CodeRabbit configuration for vyos/vyos.vyos Ansible network collection -# Docs: https://docs.coderabbit.ai/guides/configure-coderabbit - -language: en-US -early_access: false -tone_instructions: > - Concise, technical, no filler. Focus on correctness, security, idempotency, - and Ansible conventions. Cite file paths and line numbers. +# +# Per-repo CodeRabbit override for vyos/vyos.vyos (Ansible network collection). +# +# Most behavior is inherited from the org-level central baseline at +# https://github.com/vyos/coderabbit/blob/production/.coderabbit.yaml. +# This file keeps only what's distinct to this repo: +# - Ansible-collection-specific `path_instructions` (15 entries). +# - Two `path_filters` entries that aren't in the central list. +# - `base_branches: [main]` — central uses VyOS release-train names; this +# repo is an Ansible collection that lives on `main`. +# - `knowledge_base.jira` scoped to VD. +# +# Migrated from standalone (591-line rich config from T8584, sha +# 38eae56fb991b63e0c89245e4ef5fc130d3f5c8b) to centralized inheritance +# mode under T8851 on 2026-05-24. The standalone config preceded +# `vyos/coderabbit` central-config introduction (2026-05-12). + +inheritance: true reviews: - profile: chill - request_changes_workflow: false - high_level_summary: true - high_level_summary_placeholder: '@coderabbitai summary' - auto_title_placeholder: '@coderabbitai' - review_status: true - poem: false - collapse_walkthrough: true - changed_files_summary: true - sequence_diagrams: false - assess_linked_issues: true - related_issues: true - related_prs: true - suggested_labels: false - auto_apply_labels: false - suggested_reviewers: false - auto_review: - enabled: true - auto_incremental_review: true - drafts: false base_branches: - main - ignore_title_keywords: - - WIP - - DO NOT MERGE - - Bump path_filters: - - '!**/__pycache__/**' - - '!**/*.pyc' - - '!**/*.egg-info/**' + # Repo-specific filters that aren't in the central baseline. The + # central already filters `!**/__pycache__/**`, `!**/*.pyc`, + # `!**/*.egg-info/**`, `!**/.venv/**`, `!**/.worktrees/**` — so they + # are not repeated here. - '!changelogs/changelog.yaml' - - '!.venv/**' - '!.collections/**' - - '!.worktrees/**' path_instructions: # ── Global PR hygiene ────────────────────────────────────────────── @@ -209,383 +194,12 @@ reviews: not be un-tombstoned. requires_ansible must stay >=2.15.0 unless explicitly bumping minimum version. - finishing_touches: - docstrings: - enabled: true - unit_tests: - enabled: true - - tools: - github-checks: - enabled: true - timeout_ms: 90000 - eslint: - enabled: false - biome: - enabled: false - actionlint: - enabled: true - yamllint: - enabled: true - markdownlint: - enabled: true - languagetool: - enabled: true - level: default - enabled_only: false - gitleaks: - enabled: true - checkov: - enabled: false - semgrep: - enabled: true - ast-grep: - essential_rules: true - ruff: - enabled: false - -chat: - auto_reply: true knowledge_base: - opt_out: false - learnings: - scope: auto - issues: - scope: auto - pull_requests: - scope: auto - linked_repositories: - - repository: "ansible/ansible" - instructions: > - Core Ansible framework. Reference for module_utils base classes, - plugin interfaces (cliconf, terminal, action), module documentation - conventions (DOCUMENTATION/EXAMPLES/RETURN YAML blocks), and - ansible-test sanity requirements. - - repository: "ansible-collections/ansible.netcommon" - instructions: > - Network common collection. Contains ConfigBase, ResourceModule, - FactsBase, NetworkTemplate, and get_resource_connection — the base - classes and utilities that vyos.vyos modules directly extend. - -code_generation: - docstrings: - language: en-US - path_instructions: - # ── Module entry points: YAML blocks, not Python docstrings ────── - - path: 'plugins/modules/vyos_*.py' - instructions: | - Do NOT generate Python-style docstrings for these files. Ansible modules - use YAML triple-string blocks: DOCUMENTATION, EXAMPLES, and RETURN. - If updating these blocks: - - DOCUMENTATION must include: module name, author, short_description, - description (list of strings), version_added, extends_documentation_fragment - (vyos.vyos.vyos), and a full options tree with type, description, and - choices/default where applicable. Include a notes section listing supported - VyOS versions (1.3.8, 1.4.1, 1.4.2, 1.5 rolling). - - EXAMPLES must show at least one task per supported state using FQCN - (vyos.vyos.vyos_). - - RETURN must document: commands (list, always), before (dict, always), - after (dict, when changed), and any module-specific return values. - Keep version_added accurate — do not backdate. - - # ── Argspec: skip auto-generated files ─────────────────────────── - - path: 'plugins/module_utils/network/vyos/argspec/**' - instructions: | - Skip — these files are auto-generated by the Ansible resource module builder - and carry a "DO NOT EDIT" header. Do not generate or modify docstrings. - - # ── Config classes ─────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/config/**' - instructions: | - Config builder classes extending ConfigBase or ResourceModule. Use - reStructuredText-style docstrings (Ansible/Sphinx convention): - def method(self, ...): - """Short description. - - :param name: description - :type name: type - :rtype: type - :returns: description - """ - Document: execute_module(), set_config(), get__facts(), and any - method that generates CLI commands. Focus on what state transitions the - method handles and what CLI commands it may produce. Do not document trivial - __init__ that just calls super(). Some files have auto-generated headers — - keep docstrings minimal in those to avoid noise on regeneration. - - # ── Facts classes ──────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/facts/**' - instructions: | - Facts parsers that convert VyOS CLI output to structured dicts. Use rST - docstrings. Document: - - populate(): what show commands it runs and the dict structure it returns. - - render_config() / get_device_data(): the CLI command used and expected - output format. - - Any regex-heavy parsing method: briefly note what CLI patterns it handles. - Skip __init__.py files. - - # ── RM Templates ───────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/rm_templates/*.py' - instructions: | - Parser template files with _tmplt_* helper functions and PARSERS lists. - Add a module-level docstring describing the resource and VyOS CLI hierarchy - covered. For _tmplt_* functions: one-line docstring stating the VyOS command - path generated (e.g., "Generate `set protocols bgp neighbor - timers ...` commands."). Do not document individual regex PARSERS entries — - the patterns are self-describing. Files with _14 suffix target VyOS 1.4+ — - note this in the module docstring. - - # ── Cliconf plugin ────────────────────────────────────────────── - - path: 'plugins/cliconf/vyos.py' - instructions: | - Uses Ansible DOCUMENTATION block for plugin-level docs. For Python methods - use rST docstrings. Document: get_device_info(), edit_config(), get_config(), - get_diff(), commit(), discard_changes(). Note which methods enter/exit - configure mode. - - # ── Terminal and action plugins ────────────────────────────────── - - path: 'plugins/terminal/vyos.py' - instructions: | - Minimal docstrings only. Document on_open_shell() and on_close_shell() - explaining what terminal parameters they set. Regex patterns are - self-documenting — do not add inline comments to each pattern. - - - path: 'plugins/action/vyos.py' - instructions: | - Skip — thin proxy inheriting ActionNetworkModule. No docstrings needed. - - # ── Utilities ──────────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/utils/*.py' - instructions: | - Utility functions shared across modules. Use rST docstrings for any - function with non-obvious behavior. Document parameters, return types, - and side effects. Skip trivial one-liners. - - # ── Test files ────────────────────────────────────────────────── - - path: 'tests/**' - instructions: | - Skip — test files do not need docstrings. Test method names should be - self-describing (test_vyos___). - - unit_tests: - path_instructions: - # ── Resource module tests ──────────────────────────────────────── - - path: 'plugins/modules/vyos_interfaces.py' - instructions: &resource_module_tests | - Generate tests in tests/unit/modules/network/vyos/test_vyos_.py. - Inherit from TestVyosModule (import from vyos_module.py in same directory). - - Structure: - ```python - class TestVyosModule(TestVyosModule): - module = vyos_ - - def setUp(self): - super().setUp() - # Mock get_resource_connection at BOTH levels: - self.mock_get_resource_connection_config = patch( - "ansible_collections.ansible.netcommon.plugins.module_utils." - "network.common.cfg.base.get_resource_connection" - ) - self.mock_get_resource_connection_facts = patch( - "ansible_collections.ansible.netcommon.plugins.module_utils." - "network.common.facts.facts.get_resource_connection" - ) - # Mock the facts get_device_data method: - self.mock_execute_show_command = patch( - "ansible_collections.vyos.vyos.plugins.module_utils.network." - "vyos.facts....get_device_data" - ) - # Start all patches and store references - self.execute_show_command = self.mock_execute_show_command.start() - - def tearDown(self): - super().tearDown() - # Stop ALL patches - - def load_fixtures(self, commands=None, filename=None): - def load_from_file(*args, **kwargs): - return load_fixture(filename or "vyos__config.cfg") - self.execute_show_command.side_effect = load_from_file - ``` - - Required test methods for each resource module: - - test_vyos__merged: config change, changed=True, verify commands list - - test_vyos__merged_idempotent: no-op, changed=False, commands=[] - - test_vyos__replaced: replaced state, changed=True - - test_vyos__replaced_idempotent: replaced no-op, changed=False - - test_vyos__overridden: full override, changed=True - - test_vyos__deleted: deletion, changed=True - - test_vyos__gathered: state=gathered, verify result["gathered"] dict - - test_vyos__rendered: state=rendered, verify result["rendered"] commands - - test_vyos__parsed: state=parsed with running_config, verify output - - Assertions use self.execute_module(changed=True/False, commands=[...]). - Commands lists contain exact VyOS CLI strings: "set interfaces ethernet eth0 ...". - Use set_module_args(dict(config=[...], state="")) before execute_module. - Create fixture files in tests/unit/modules/network/vyos/fixtures/ named - vyos__config.cfg with valid VyOS set-syntax configuration. - Use load_fixture() to read fixtures — never inline raw config. - - - path: 'plugins/modules/vyos_l3_interfaces.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_lag_interfaces.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_lldp_global.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_lldp_interfaces.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_static_routes.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_firewall_rules.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_firewall_global.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_firewall_interfaces.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_ospfv2.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_ospfv3.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_ospf_interfaces.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_bgp_global.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_bgp_address_family.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_prefix_lists.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_route_maps.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_snmp_server.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_logging_global.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_ntp_global.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_hostname.py' - instructions: *resource_module_tests - - - path: 'plugins/modules/vyos_vrf.py' - instructions: *resource_module_tests - - # ── Legacy module tests ────────────────────────────────────────── - - path: 'plugins/modules/vyos_command.py' - instructions: &legacy_module_tests | - Generate tests in tests/unit/modules/network/vyos/test_vyos_.py. - Inherit from TestVyosModule. - - Legacy modules mock differently from resource modules: - ```python - class TestVyosModule(TestVyosModule): - module = vyos_ - - def setUp(self): - super().setUp() - # Mock run_commands directly on the module: - self.mock_run_commands = patch( - "ansible_collections.vyos.vyos.plugins.modules." - "vyos_.run_commands" - ) - self.run_commands = self.mock_run_commands.start() - # Some also mock get_capabilities or get_config/load_config - - def tearDown(self): - super().tearDown() - self.mock_run_commands.stop() - - def load_fixtures(self, commands=None, filename=None): - # Set run_commands return_value or side_effect - self.run_commands.return_value = [load_fixture(filename)] - ``` - - Legacy modules (vyos_command, vyos_config, vyos_facts, vyos_banner, - vyos_ping, vyos_system, vyos_user, vyos_vlan) do not use - resource module states. Test: successful execution, error handling, - idempotency where applicable, and specific module features (e.g., - vyos_command wait_for/retries, vyos_config src/lines/match). - Use execute_module(changed=, commands=) for assertions. - - - path: 'plugins/modules/vyos_config.py' - instructions: *legacy_module_tests - - - path: 'plugins/modules/vyos_facts.py' - instructions: *legacy_module_tests - - - path: 'plugins/modules/vyos_banner.py' - instructions: *legacy_module_tests - - - path: 'plugins/modules/vyos_ping.py' - instructions: *legacy_module_tests - - - path: 'plugins/modules/vyos_system.py' - instructions: *legacy_module_tests - - - path: 'plugins/modules/vyos_user.py' - instructions: *legacy_module_tests - - - path: 'plugins/modules/vyos_vlan.py' - instructions: *legacy_module_tests - - # ── Test infrastructure — do not generate tests for these ──────── - - path: 'tests/unit/modules/utils.py' - instructions: | - Skip — test infrastructure (ModuleTestCase base, set_module_args, exception - classes). Do not generate tests for test utilities. - - - path: 'tests/unit/modules/conftest.py' - instructions: | - Skip — pytest fixtures (patch_ansible_module). Do not generate tests. - - - path: 'tests/unit/modules/network/vyos/vyos_module.py' - instructions: | - Skip — TestVyosModule base class with execute_module(), load_fixture(), - and mock setup. Do not generate tests for the test base class. - - - path: 'tests/unit/modules/network/vyos/fixtures/**' - instructions: | - Skip — raw VyOS CLI output fixtures. Not code, not testable. - - # ── Non-module plugin code ─────────────────────────────────────── - - path: 'plugins/module_utils/**' - instructions: | - Module utility code (argspec, config, facts, rm_templates, utils). - These are tested indirectly through module-level tests — the config - classes are exercised when test_vyos_.py calls execute_module(). - Do not generate separate unit tests for module_utils classes unless - a utility function in plugins/module_utils/network/vyos/utils/ has - complex standalone logic worth testing in isolation. - - - path: 'plugins/cliconf/vyos.py' - instructions: | - Skip — cliconf plugin is tested via integration tests and indirectly - through module tests. Unit testing requires complex CliconfBase mocking - that provides little value over integration coverage. - - - path: 'plugins/terminal/vyos.py' - instructions: | - Skip — terminal plugin regex patterns are validated through integration - tests against actual VyOS devices. - - - path: 'plugins/action/vyos.py' - instructions: | - Skip — thin action proxy. Tested indirectly via module tests. + jira: + # `auto` activates Jira context lookups when this repo lives on an + # org with an Atlassian OAuth grant attached (VyOS-Networks); on the + # public vyos source it self-disables. + usage: auto + project_keys: + - VD diff --git a/changelogs/fragments/t8851-coderabbit-centralized.yml b/changelogs/fragments/t8851-coderabbit-centralized.yml new file mode 100644 index 00000000..b9be774e --- /dev/null +++ b/changelogs/fragments/t8851-coderabbit-centralized.yml @@ -0,0 +1,6 @@ +trivial: + - Migrate ``.coderabbit.yaml`` from standalone configuration to centralized + inheritance under the org-level baseline at ``vyos/coderabbit``. No effect + on consumers; the file shrinks from 591 lines to 205 while preserving all + Ansible-collection-specific ``path_instructions`` and adding a + ``knowledge_base.jira`` block scoped to ``VD`` (T8851). -- cgit v1.2.3 From c5ffa674c6516ce710cdf491fba646e11477af13 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Mon, 25 May 2026 22:42:14 +0300 Subject: T8519: add changelog fragment vyos_vlan (#462) * T8519: add changelog fragment for new unit tests * Update missing unit tests entry in changelog Removed mention of unit tests for vyos_l3_interfaces and vyos_lldp_interfaces. --------- Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- changelogs/fragments/missing-unit-tests.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelogs/fragments/missing-unit-tests.yml diff --git a/changelogs/fragments/missing-unit-tests.yml b/changelogs/fragments/missing-unit-tests.yml new file mode 100644 index 00000000..a83b336c --- /dev/null +++ b/changelogs/fragments/missing-unit-tests.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - Add unit tests for vyos_vlan module. -- cgit v1.2.3 From fd226fc4f3a0fd7639794b0eacaf17c972640049 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Tue, 26 May 2026 22:37:40 +0300 Subject: T8514: fix typo in meta/runtime.yml snmp_server redirect (#457) Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- changelogs/fragments/fix-snmp-server-redirect.yml | 3 +++ meta/runtime.yml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/fix-snmp-server-redirect.yml diff --git a/changelogs/fragments/fix-snmp-server-redirect.yml b/changelogs/fragments/fix-snmp-server-redirect.yml new file mode 100644 index 00000000..59147f4a --- /dev/null +++ b/changelogs/fragments/fix-snmp-server-redirect.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - Fix meta/runtime.yml redirect for snmp_server pointing to non-existent vyos_snmp_servers module. diff --git a/meta/runtime.yml b/meta/runtime.yml index 36579e05..f0a53ee7 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -55,7 +55,7 @@ plugin_routing: prefix_lists: redirect: vyos.vyos.vyos_prefix_lists snmp_server: - redirect: vyos.vyos.vyos_snmp_servers + redirect: vyos.vyos.vyos_snmp_server static_routes: redirect: vyos.vyos.vyos_static_routes system: -- cgit v1.2.3 From 8e3b18f034d7183d6d756c10ec8c7c93b574d01d Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Thu, 28 May 2026 13:09:00 +0300 Subject: T8615: add Mergify config (extends: mergify central template) (#476) * T8615: add Mergify config (extends: mergify central template) Adds the org-local central-config inheritance file per the T8615 sweep (re-scoped 2026-05-28). 4-line minimum: extends: mergify resolves to vyos/mergify which provides commands_restrictions (9 slash commands x 4-sender allowlist), conflict labeler, PR-title T-ID format check, opt-in auto-update, and the backport-conflict merge protection. merge_protections_settings: reporting_method: check-runs declared explicitly per-repo because only defaults and commands_restrictions are documented as merging across extends: -- pin to current behavior pre the 2026-07-31 default flip per data/github.md Mergify gotchas table. Phase 0 CR: 0 finding(s). Known false positive: CR flags extends: mergify as 'not a documented built-in preset' but Mergify documents the org-local form at https://docs.mergify.com/configuration/sharing/. Refs: T8615 (re-scoped 2026-05-28), IS-421 Generated by robots https://vyos.io * T8615: switch yaml-language-server schema URL to JSON-schema Replaces the human-docs file-format URL (which serves HTML and cannot drive editor validation) with the machine-readable JSON-schema URL that yaml-language-server actually resolves for autocomplete and schema validation. Old: https://docs.mergify.com/configuration/file-format/ New: https://docs.mergify.com/mergify-configuration-schema.json Phase 0 CR: 0 0 finding(s). Refs: T8615 (re-scoped 2026-05-28), IS-421 Generated by robots https://vyos.io --- .github/mergify.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/mergify.yml diff --git a/.github/mergify.yml b/.github/mergify.yml new file mode 100644 index 00000000..525594cc --- /dev/null +++ b/.github/mergify.yml @@ -0,0 +1,4 @@ +# yaml-language-server: $schema=https://docs.mergify.com/mergify-configuration-schema.json +extends: mergify +merge_protections_settings: + reporting_method: check-runs -- cgit v1.2.3 From c1e3b4d9009bda56b21bcb8f34d936393f9d7650 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 30 May 2026 16:06:21 +0300 Subject: ci: T8943: sweep HIGH-producer pins to renamed branches (rollout 1c) (#477) Rewrites uses: pins to the three HIGH-fanout producers (vyos/.github, vyos/vyos-cla-signatures, VyOS-Networks/vyos-reusable-workflows) from their old default branch to the new production compat branch staged in Task 1. No functional change; pin-ref rewrite only. Tracking: T8943 --- .github/workflows/cla-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml index da3e6efd..fa96d19d 100644 --- a/.github/workflows/cla-check.yml +++ b/.github/workflows/cla-check.yml @@ -11,5 +11,5 @@ on: types: [created] jobs: call-cla-assistant: - uses: vyos/vyos-cla-signatures/.github/workflows/cla-reusable.yml@current + uses: vyos/vyos-cla-signatures/.github/workflows/cla-reusable.yml@production secrets: inherit -- cgit v1.2.3 From 403e6e2d909691177265f2d745bee2df865e5b1e Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Mon, 1 Jun 2026 03:03:46 +0300 Subject: docs: AGENTS.md: reflect rollout 1c default-branch rename (#478) --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 436b6785..f14e1246 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ The official Ansible Collection for managing VyOS network appliances (`vyos.vyos ## Cross-repo context - Consumed by Ansible users running playbooks against VyOS routers built by `vyos/vyos-build`. -- The `vyos.vyos` collection talks to VyOS via `network_cli` connections; supports the same train branches (`current`, `circinus`, `sagitta`, `equuleus`). +- The `vyos.vyos` collection talks to VyOS via `network_cli` connections; supports the same train branches (`rolling`, `circinus`, `sagitta`, `equuleus`). ## Conventions - Commit headline: `T12345: description` (Phorge ID at https://vyos.dev mandatory). No workflow enforces PR title format in this repo. -- cgit v1.2.3 From 55758ebb3578d01db3e54a052eb6c2ab775162ca Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 6 Jun 2026 00:00:34 +0300 Subject: ci: T8966: add product T-ID title/commit check (opt-in relocation) (#481) --- .github/mergify.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/mergify.yml b/.github/mergify.yml index 525594cc..bd832b72 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -2,3 +2,22 @@ extends: mergify merge_protections_settings: reporting_method: check-runs + +pull_request_rules: + - name: Flag product T-ID format violation in PR title or commit messages + description: > + Product-repo convention: PR title and every commit's first line must + match `T: ` (optional `scope: ` prefix). Relocated from + the central config (T8966) so the T-ID convention is opt-in per product + repo. Name is intentionally distinct from any central rule name so this + stays additive (not an `extends:` override). + conditions: + - '-closed' + - '-merged' + - or: + - '-title~=^(([a-zA-Z0-9\-_.]+:\s)?)T\d+:\s+[^\s]+.*' + - 'commits[*].commit_message~=^(?!(([a-zA-Z0-9\-_.]+:\s)?)T\d+:\s+[^\s]+).*' + actions: + label: + toggle: + - invalid-task-id -- cgit v1.2.3 From 69fccf7db6857106e13fa29fee63176c3b52248d Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 6 Jun 2026 19:02:37 +0300 Subject: ci: T8966: exempt bots from product T-ID (invalid-task-id) gate (#482) --- .github/mergify.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/mergify.yml b/.github/mergify.yml index bd832b72..49298b52 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -14,6 +14,9 @@ pull_request_rules: conditions: - '-closed' - '-merged' + - '-author~=\[bot\]$' + - 'author!=copilot-swe-agent' + - 'author!=vyosbot' - or: - '-title~=^(([a-zA-Z0-9\-_.]+:\s)?)T\d+:\s+[^\s]+.*' - 'commits[*].commit_message~=^(?!(([a-zA-Z0-9\-_.]+:\s)?)T\d+:\s+[^\s]+).*' -- cgit v1.2.3 From 95d335540ff8dd9c7d2ad618c8d9b569f15c6252 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Mon, 8 Jun 2026 01:34:47 +0300 Subject: T8966: add legacy-label escape to invalid-task-id rule (commit check exempt) (#483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-repo T-ID rule flagged every commit's first line, but on repos that squash-merge AND block force-push (vyos.vyos enforces non_fast_forward on ~ALL branches, zero bypass) authors cannot rewrite history to make existing commits conform. The label became unclearable. Keep the title check (always fixable), and exempt the per-commit check when a maintainer applies the new `legacy` label. New PRs are still nudged toward the convention. 🤖 Generated by [robots](https://vyos.io) --- .github/mergify.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/mergify.yml b/.github/mergify.yml index 49298b52..43353ea4 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -6,11 +6,17 @@ merge_protections_settings: pull_request_rules: - name: Flag product T-ID format violation in PR title or commit messages description: > - Product-repo convention: PR title and every commit's first line must - match `T: ` (optional `scope: ` prefix). Relocated from - the central config (T8966) so the T-ID convention is opt-in per product - repo. Name is intentionally distinct from any central rule name so this - stays additive (not an `extends:` override). + Product-repo convention: the PR title AND every commit's first line must + match `T: ` (optional `scope: ` prefix). The title is always + checked; the per-commit check is exempted when a maintainer applies the + `legacy` label — an escape hatch for grandfathered PRs whose commit history + cannot be rewritten (repos that squash-merge AND block force-push, e.g. + vyos.vyos enforces `non_fast_forward` on ~ALL branches with zero bypass). + New PRs are still nudged toward the convention; `legacy` is the deliberate, + maintainer-controlled opt-out. Relocated from the central config (T8966) + so the convention is opt-in per product repo. Name is intentionally + distinct from any central rule name so this stays additive (not an + `extends:` override). Legacy-label escape added 2026-06-08 (T8966). conditions: - '-closed' - '-merged' @@ -19,7 +25,9 @@ pull_request_rules: - 'author!=vyosbot' - or: - '-title~=^(([a-zA-Z0-9\-_.]+:\s)?)T\d+:\s+[^\s]+.*' - - 'commits[*].commit_message~=^(?!(([a-zA-Z0-9\-_.]+:\s)?)T\d+:\s+[^\s]+).*' + - and: + - 'label!=legacy' + - 'commits[*].commit_message~=^(?!(([a-zA-Z0-9\-_.]+:\s)?)T\d+:\s+[^\s]+).*' actions: label: toggle: -- cgit v1.2.3 From 6e53f1eaaab328d38a5c5fd82a29b77b34d5143b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:03:15 +0000 Subject: Bump codecov/codecov-action from 6 to 7 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6 to 7. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v6...v7) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codecoverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml index 8ce3af36..ee71e179 100644 --- a/.github/workflows/codecoverage.yml +++ b/.github/workflows/codecoverage.yml @@ -62,7 +62,7 @@ jobs: working-directory: ${{ steps.identify.outputs.collection_path }} - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: directory: ${{ steps.identify.outputs.collection_path }} fail_ci_if_error: false -- cgit v1.2.3 From 5b80cd0cbddd014e54b01e7fa1cd0c0777d94724 Mon Sep 17 00:00:00 2001 From: Robert Navarro Date: Fri, 19 Jun 2026 06:02:27 -0700 Subject: rm_templates: T8609: fix slow parse() from group-quantifier patterns (#470) * rm_templates: T8609: fix slow parse() from group-quantifier patterns Running an Ansible playbook that manages BGP route-maps on my VyOS 1.4 edge routers, the vyos.vyos.vyos_route_maps task (state=gathered or state=merged) takes ~50 seconds per host, sometimes 100s+, against devices with only a handful of route-maps configured. The persistent connection times out before the module finishes and the failure surfaces as a misleading "socket path does not exist". Three related quantifier shapes in nine rm_templates files cause O(2^n) regex backtracking on inputs that share a parser's prefix but don't match overall. parse() time on a representative 12-line route-map config drops from ~50s to <1ms post-fix. * Trailing form (188 sites): `(?P\S+)\n *$"""` -> `(?P\S+)\s*$"""`. Under re.VERBOSE the literal newline+indent between `\S+` and `*$` is stripped at compile time, so the source compiled to `(\S+)*$` with the `*` quantifying the named group. * Mid-pattern `(group containing \S+)*` (22 sites in snmp_server.py with a couple in bgp_global*.py): e.g. `(?Pprotocol\s\S+)*` -> `(?Pprotocol\s\S+)?`. Different position from the trailing form, but the same shape underneath (a group whose content includes \S+, quantified with `*`), so the same O(2^n) backtracking on prefix-sharing inputs. * Mid-pattern `(literal-only group)*` (14 sites in snmp_server.py, bgp_address_family*.py, bgp_global*.py): e.g. `(?Pas-set)*` -> `(?Pas-set)?`. No \S+ inside the quantified group, so the backtracking exposure is much smaller. On the inputs these patterns actually receive (no device line carries duplicate flags) `*` and `?` accept identical input sets, so changing to `?` is behavior-preserving. The first draft of this fix made set_comm_list_delete's `(?P\S+)` group required. The parser's setval emits `set comm-list delete` with no token after `delete`, so the parser stopped matching its own output (no existing fixture covered this case, which is why the regression initially shipped). CodeRabbit caught it during review. Replaced `delete(?P\S+)\s*$` with `\s(?Pdelete)\s*$` so the named group captures the literal word `delete`; the result template `{{True if delete is defined}}` continues to evaluate True. Affects: route_maps[/_14], bgp_global[/_14], bgp_address_family[/_14], snmp_server, ospf_interfaces[/_14]. Adds tests/unit/modules/network/vyos/test_rm_templates_perf.py with a 1-second budget against fixture inputs that have realistic-length identifiers, plus a round-trip test asserting set_comm_list_delete matches its own setval-generated line, plus a Bgp_address_familyTemplate14 budget test for symmetry with the other "hot" template families. * Fix regex backtracking issues in parse() method Fix slow parse() to prevent regex backtracking on prefix-sharing inputs across multiple files. Add unit test for the bugfixes. * Refactor bug fixes for regex backtracking improvements Updated bug fixes to include details on regex backtracking issues in multiple files. * Fix typo in bugfixes section of changelog * Update bugfixes for regex backtracking issues * rm_templates: T8609: route_maps.py: fix remaining trailing `*$` patterns Five sites in route_maps.py still had the `(?P...)\n *$` shape that collapses to `(?P...)*$` under `re.VERBOSE`, parsers `sequence`, `on_match_next`, `set_atomic_aggregate`, `set_extcommunity_bandwidth_non_transitive`, and `match_community_exact_match`. Collapsed each to `\s*$` on the same line as the named group, matching the rest of the PR. * rm_templates: T8609: route_maps.py: split overlong getval to satisfy E501 Line 517 was 161 chars (`set_extcommunity_bandwidth_non_transitive` parser) and tripped pycodestyle's E501 sanity test in CI. Split the regex source across two lines at the `\d+)` boundary; under `re.VERBOSE` the literal newline and indent between regex tokens are stripped at compile time, so the engine sees the same pattern. `\s*$` stays on the same line as the closing capture group, so the group-quantifier shape this PR fixes elsewhere isn't reintroduced. --------- Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- .../T8609_rm_templates_regex_backtracking.yml | 12 ++ .../vyos/rm_templates/bgp_address_family.py | 49 +++---- .../vyos/rm_templates/bgp_address_family_14.py | 49 +++---- .../network/vyos/rm_templates/bgp_global.py | 95 ++++-------- .../network/vyos/rm_templates/bgp_global_14.py | 95 ++++-------- .../network/vyos/rm_templates/ospf_interfaces.py | 9 +- .../vyos/rm_templates/ospf_interfaces_14.py | 12 +- .../network/vyos/rm_templates/route_maps.py | 145 +++++++----------- .../network/vyos/rm_templates/route_maps_14.py | 123 ++++++---------- .../network/vyos/rm_templates/snmp_server.py | 78 +++++----- .../modules/network/vyos/test_rm_templates_perf.py | 163 +++++++++++++++++++++ 11 files changed, 405 insertions(+), 425 deletions(-) create mode 100644 changelogs/fragments/T8609_rm_templates_regex_backtracking.yml create mode 100644 tests/unit/modules/network/vyos/test_rm_templates_perf.py diff --git a/changelogs/fragments/T8609_rm_templates_regex_backtracking.yml b/changelogs/fragments/T8609_rm_templates_regex_backtracking.yml new file mode 100644 index 00000000..b02dbf19 --- /dev/null +++ b/changelogs/fragments/T8609_rm_templates_regex_backtracking.yml @@ -0,0 +1,12 @@ +--- +bugfixes: + - bgp_address_family.py - Fix slow parse() to stop regex backtracking on prefix-sharing inputs. + - bgp_address_family_14.py - Fix slow parse() to stop regex backtracking on prefix-sharing inputs. + - bgp_global.py - Fix slow parse() to stop regex backtracking on prefix-sharing inputs. + - bgp_global_14.py - Fix slow parse() to stop regex backtracking on prefix-sharing inputs. + - ospf_interfaces.py - Fix slow parse() to stop regex backtracking on prefix-sharing inputs. + - ospf_interfaces_14.py - Fix slow parse() to stop regex backtracking on prefix-sharing inputs. + - route_maps.py - Fix slow parse() to stop regex backtracking on prefix-sharing inputs. + - route_maps_14.py - Fix slow parse() to stop regex backtracking on prefix-sharing inputs. + - snmp_server.py - Fix slow parse() to stop regex backtracking on prefix-sharing inputs. + - test_rm_templates_perf.py - Add unit test for the bugfixes diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py index f8f86cd2..fccfc9c2 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py @@ -293,8 +293,8 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P\S+)-unicast \s+aggregate-address \s+(?P
\S+) - \s*(?Pas-set)* - \s*(?Psummary-only)* + \s*(?Pas-set)? + \s*(?Psummary-only)? $""", re.VERBOSE, ), @@ -397,8 +397,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+network \s+(?P
\S+) \s+path-limit - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -432,8 +431,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+network \s+(?P
\S+) \s+route-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -499,8 +497,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+redistribute \s+(?P\S+) \s+metric - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -534,8 +531,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+redistribute \s+(?P\S+) \s+route-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -568,8 +564,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P\S+)-unicast \s+redistribute \s+table - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -659,8 +654,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P\S+)-unicast \s+allowas-in \s+number - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -877,8 +871,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P\S+)-unicast \s+capability \s+prefix-list - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -915,8 +908,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P\S+)-unicast \s+default-originate \s+route-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -992,8 +984,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P\S+)-unicast \s+prefix-list \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_prefix_list, @@ -1033,8 +1024,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P\S+)-unicast \s+filter-list \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_filter_list, @@ -1073,8 +1063,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P\S+)-unicast \s+maximum-prefix - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1176,8 +1165,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P\S+)-unicast \s+peer-group - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1246,8 +1234,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P\S+)-unicast \s+route-map \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_route_map, @@ -1389,8 +1376,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P\S+)-unicast \s+unsuppress-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1424,8 +1410,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P\S+)-unicast \s+weight - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py index fd4c9de9..09cd41a7 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py @@ -309,8 +309,8 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P\S+)-unicast \s+aggregate-address \s+(?P
\S+) - \s*(?Pas-set)* - \s*(?Psummary-only)* + \s*(?Pas-set)? + \s*(?Psummary-only)? $""", re.VERBOSE, ), @@ -410,8 +410,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+network \s+(?P
\S+) \s+path-limit - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -444,8 +443,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+network \s+(?P
\S+) \s+route-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -508,8 +506,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P\S+)-unicast \s+redistribute \s+(?P\S+) - \s+metric\s+(?P\S+) - *$""", + \s+metric\s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -542,8 +539,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+redistribute \s+(?P\S+) \s+route-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -575,8 +571,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P\S+)-unicast \s+redistribute \s+table - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -663,8 +658,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P\S+)-unicast \s+allowas-in \s+number - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -875,8 +869,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P\S+)-unicast \s+capability \s+prefix-list - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -912,8 +905,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P\S+)-unicast \s+default-originate \s+route-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -987,8 +979,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P\S+)-unicast \s+prefix-list \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_prefix_list, @@ -1027,8 +1018,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P\S+)-unicast \s+filter-list \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_filter_list, @@ -1066,8 +1056,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P\S+)-unicast \s+maximum-prefix - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1166,8 +1155,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P\S+)-unicast \s+peer-group - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1234,8 +1222,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P\S+)-unicast \s+route-map \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_route_map, @@ -1373,8 +1360,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P\S+)-unicast \s+unsuppress-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1407,8 +1393,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P\S+)-unicast \s+weight - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_global.py b/plugins/module_utils/network/vyos/rm_templates/bgp_global.py index 8c2e2f55..03f6265b 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_global.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_global.py @@ -237,8 +237,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+advertisement-interval - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} advertisement-interval {{ neighbor.advertisement_interval }}", @@ -437,8 +436,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+capability \s+orf \s+prefix-list - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} capability orf prefix-list {{ neighbor.capability.orf }}", @@ -467,8 +465,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P
\S+) \s+default-originate \s+route-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} default-originate route-map {{ neighbor.default_originate }}", @@ -494,8 +491,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+description - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} description {{ neighbor.description }}", @@ -573,8 +569,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+disable-send-community - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} disable-send-community {{ neighbor.disable_send_community }}", @@ -633,8 +628,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+ebgp-multihop - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} ebgp-multihop {{ neighbor.ebgp_multihop }}", @@ -661,8 +655,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P
\S+) \s+filter-list \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_filter_list, @@ -721,8 +714,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+maximum-prefix - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} maximum-prefix {{ neighbor.maximum_prefix }}", @@ -826,8 +818,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+password - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} password {{ neighbor.password }}", @@ -853,8 +844,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+peer-group - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} peer-group {{ neighbor.peer_group_name }}", @@ -880,8 +870,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+port - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} port {{ neighbor.port }}", @@ -908,8 +897,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P
\S+) \s+prefix-list \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_prefix_list, @@ -940,8 +928,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+remote-as - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} remote-as {{ neighbor.remote_as }}", @@ -994,8 +981,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P
\S+) \s+route-map \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_route_map, @@ -1157,8 +1143,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+unsuppress-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} unsuppress-map {{ neighbor.unsuppress_map }}", @@ -1184,8 +1169,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+update-source - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} update-source {{ neighbor.update_source }}", @@ -1211,8 +1195,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+weight - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} weight {{ neighbor.weight }}", @@ -1238,8 +1221,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+ttl-security - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} ttl-security {{ neighbor.ttl_security }}", @@ -1266,8 +1248,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P
\S+) \s+timers \s+(?Pconnect|holdtime|keepalive) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_timers, @@ -1295,8 +1276,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P\d+) \s+timers \s+(?P\S+) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_timers, @@ -1418,8 +1398,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P\d+) \s+parameters \s+cluster-id - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters cluster-id {{ bgp_params.cluster_id }}", @@ -1442,8 +1421,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+confederation \s+(?Pidentifier|peers) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_confederation, @@ -1471,8 +1449,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+half-life - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening half-life {{ bgp_params.dampening.half_life}}", @@ -1497,8 +1474,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+max-suppress-time - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening max-suppress-time {{ bgp_params.dampening.max_suppress_time}}", @@ -1523,8 +1499,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+re-use - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening re-use {{ bgp_params.dampening.re_use}}", @@ -1549,8 +1524,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+start-suppress-time - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening start-suppress-time {{ bgp_params.dampening.start_suppress_time}}", @@ -1574,9 +1548,8 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P\d+) \s+parameters \s+default - \s*(?Pno-ipv4-unicast)* - \s*(?Plocal-pref\s\S+) - *$""", + \s*(?Pno-ipv4-unicast)? + \s*(?Plocal-pref\s\S+)?\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_default, @@ -1648,8 +1621,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+distance\sprefix \s+(?P\S+) \s+distance - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters distance prefix {{ bgp_params.distance.prefix }} distance {{ bgp_params.distance.value }}", @@ -1678,8 +1650,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+distance\sglobal \s+(?P\S+) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_distance, @@ -1729,8 +1700,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P\d+) \s+parameters \s+graceful-restart\s+stalepath-time - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters graceful-restart stalepath-time {{ bgp_params.graceful_restart }}", @@ -1818,8 +1788,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P\d+) \s+parameters \s+router-id - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters router-id {{ bgp_params.router_id }}", diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py b/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py index 39855c1c..cbe21e3a 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py @@ -234,8 +234,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+advertisement-interval - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} advertisement-interval {{ neighbor.advertisement_interval }}", @@ -427,8 +426,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+capability \s+orf \s+prefix-list - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} capability orf prefix-list {{ neighbor.capability.orf }}", @@ -456,8 +454,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P
\S+) \s+default-originate \s+route-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} default-originate route-map {{ neighbor.default_originate }}", @@ -482,8 +479,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+description - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} description {{ neighbor.description }}", @@ -558,8 +554,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+disable-send-community - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} disable-send-community {{ neighbor.disable_send_community }}", @@ -616,8 +611,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+ebgp-multihop - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} ebgp-multihop {{ neighbor.ebgp_multihop }}", @@ -643,8 +637,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P
\S+) \s+filter-list \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_filter_list, @@ -701,8 +694,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+maximum-prefix - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} maximum-prefix {{ neighbor.maximum_prefix }}", @@ -802,8 +794,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+password - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} password {{ neighbor.password }}", @@ -828,8 +819,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+peer-group - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} peer-group {{ neighbor.peer_group_name }}", @@ -854,8 +844,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+port - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} port {{ neighbor.port }}", @@ -881,8 +870,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P
\S+) \s+prefix-list \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_prefix_list, @@ -912,8 +900,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+remote-as - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} remote-as {{ neighbor.remote_as }}", @@ -964,8 +951,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P
\S+) \s+route-map \s+(?Pexport|import) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_route_map, @@ -1121,8 +1107,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+unsuppress-map - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} unsuppress-map {{ neighbor.unsuppress_map }}", @@ -1147,8 +1132,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+update-source - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} update-source {{ neighbor.update_source }}", @@ -1173,8 +1157,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+weight - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} weight {{ neighbor.weight }}", @@ -1199,8 +1182,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P
\S+) \s+ttl-security - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} ttl-security {{ neighbor.ttl_security }}", @@ -1226,8 +1208,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P
\S+) \s+timers \s+(?Pconnect|holdtime|keepalive) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_timers, @@ -1254,8 +1235,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+timers \s+(?P\S+) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_timers, @@ -1372,8 +1352,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+cluster-id - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters cluster-id {{ bgp_params.cluster_id }}", @@ -1395,8 +1374,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+confederation \s+(?Pidentifier|peers) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_confederation, @@ -1423,8 +1401,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+half-life - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening half-life {{ bgp_params.dampening.half_life}}", @@ -1448,8 +1425,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+max-suppress-time - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening max-suppress-time {{ bgp_params.dampening.max_suppress_time}}", @@ -1473,8 +1449,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+re-use - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening re-use {{ bgp_params.dampening.re_use}}", @@ -1498,8 +1473,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+start-suppress-time - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening start-suppress-time {{ bgp_params.dampening.start_suppress_time}}", @@ -1522,9 +1496,8 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+default - \s*(?Pno-ipv4-unicast)* - \s*(?Plocal-pref\s\S+) - *$""", + \s*(?Pno-ipv4-unicast)? + \s*(?Plocal-pref\s\S+)?\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_default, @@ -1593,8 +1566,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+distance\sprefix \s+(?P\S+) \s+distance - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters distance prefix {{ bgp_params.distance.prefix }} distance {{ bgp_params.distance.value }}", @@ -1622,8 +1594,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+distance\sglobal \s+(?P\S+) - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_distance, @@ -1671,8 +1642,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+graceful-restart\s+stalepath-time - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters graceful-restart stalepath-time {{ bgp_params.graceful_restart }}", @@ -1756,8 +1726,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+router-id - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters router-id {{ bgp_params.router_id }}", diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py index 134effca..af04da3b 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py @@ -365,8 +365,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+(?Pospf|ospfv3) \s+authentication \s+plaintext-password - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_password, @@ -399,8 +398,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+key-id \s+(?P\d+) \s+md5-key - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_md5, @@ -572,8 +570,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+(?Pip|ipv6) \s+(?Pospf|ospfv3) \s+network - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_network, diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py index 0d3aa5a7..484d7c44 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py @@ -240,8 +240,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+protocols \s+(?Pospf|ospfv3) \s+interface - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "remval": _tmplt_ospf_int_delete, @@ -266,8 +265,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+(?P\S+) \s+authentication \s+plaintext-password - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_password, @@ -298,8 +296,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+key-id \s+(?P\d+) \s+md5-key - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_md5, @@ -459,8 +456,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+interface \s+(?P\S+) \s+network - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_network, diff --git a/plugins/module_utils/network/vyos/rm_templates/route_maps.py b/plugins/module_utils/network/vyos/rm_templates/route_maps.py index c6b88f7b..51d0ea1c 100644 --- a/plugins/module_utils/network/vyos/rm_templates/route_maps.py +++ b/plugins/module_utils/network/vyos/rm_templates/route_maps.py @@ -33,8 +33,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "route_map", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "route_map", @@ -51,8 +50,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "sequence", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\s*$""", re.VERBOSE, ), "compval": "sequence", @@ -75,8 +73,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "call", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\scall\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\scall\s(?P\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} call {{call}}", @@ -99,8 +96,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "description", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sdescription\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sdescription\s(?P\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} description {{description}}", @@ -123,8 +119,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "action", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\saction\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\saction\s(?P\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} action {{action}}", @@ -147,8 +142,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "continue_sequence", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\scontinue\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\scontinue\s(?P\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} continue {{continue_sequence}}", @@ -171,8 +165,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "on_match_next", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\son-match\s(?Pnext) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\son-match\s(?Pnext)\s*$""", re.VERBOSE, ), "compval": "on_match.next", @@ -198,8 +191,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "on_match_goto", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\son-match\sgoto\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\son-match\sgoto\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "on_match.goto", @@ -225,8 +217,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_aggregator_ip", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\saggregator\sip\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\saggregator\sip\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.aggregator.ip", @@ -254,8 +245,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_aggregator_as", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\saggregator\sas\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\saggregator\sas\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.aggregator.as", @@ -283,8 +273,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_as_path_exclude", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sas-path-exclude\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sas-path-exclude\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.as_path_exclude", @@ -337,8 +326,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_atomic_aggregate", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\s(?Patomic-aggregate) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\s(?Patomic-aggregate)\s*$""", re.VERBOSE, ), "compval": "set.atomic_aggregate", @@ -364,8 +352,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_bgp_extcommunity_rt", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sbgp-extcommunity-rt\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sbgp-extcommunity-rt\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.bgp_extcommunity_rt", @@ -392,8 +379,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_comm_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\scommunity-list\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\scommunity-list\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.community.community_list", @@ -420,8 +406,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_comm_list_delete", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\scomm-list\sdelete(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\scomm-list\s(?Pdelete)\s*$""", re.VERBOSE, ), "compval": "set.comm_list.comm_list", @@ -448,8 +433,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_extcommunity_rt", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity-rt\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity-rt\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_rt", @@ -476,8 +460,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_extcommunity_soo", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity-soo\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity-soo\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_soo", @@ -504,8 +487,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_extcommunity_bandwidth", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity\sbandwidth\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity\sbandwidth\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_bandwidth", @@ -532,8 +514,8 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_extcommunity_bandwidth_non_transitive", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity\s(?Pbandwidth-non-transitive) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+) + \sset\sextcommunity\s(?Pbandwidth-non-transitive)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_bandwidth_non_transitive", @@ -560,8 +542,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_ip_next_hop", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sip-next-hop\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sip-next-hop\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.ip_next_hop", @@ -590,8 +571,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sipv6-next-hop \s(?Pglobal|local) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.ipv6_next_hop", @@ -621,8 +601,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_large_community", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\slarge-community\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\slarge-community\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.large_community", @@ -649,8 +628,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_local_preference", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\slocal-preference\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\slocal-preference\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.local_preference", @@ -677,8 +655,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_metric", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\smetric\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\smetric\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.metric", @@ -705,8 +682,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_metric_type", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\smetric-type\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\smetric-type\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.metric_type", @@ -733,8 +709,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_origin", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sorigin\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sorigin\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.origin", @@ -761,8 +736,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_originator_id", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\soriginator-id\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\soriginator-id\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.originator_id", @@ -789,8 +763,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_src", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\ssrc\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\ssrc\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.src", @@ -817,8 +790,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_tag", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\stag\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\stag\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.tag", @@ -845,8 +817,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_weight", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sweight\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sweight\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.weight", @@ -873,8 +844,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_table", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\stable\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\stable\s(?P
\S+)\s*$""", re.VERBOSE, ), "compval": "set.weight", @@ -901,8 +871,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_community", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\scommunity\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\scommunity\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.community.value", @@ -931,8 +900,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_as_path", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sas-path\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sas-path\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.as_path", @@ -959,8 +927,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_community_community_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\scommunity-list\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\scommunity-list\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.community.community_list", @@ -987,8 +954,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_community_exact_match", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\sexact-match(?P) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\sexact-match(?P)\s*$""", re.VERBOSE, ), "compval": "match.community.exact_match", @@ -1015,8 +981,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_extcommunity", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sextcommunity\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sextcommunity\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.extcommunity", @@ -1043,8 +1008,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_interface", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sinterface\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sinterface\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.interface", @@ -1071,8 +1035,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_large_community_large_community_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\slarge-community\slarge-community-list\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\slarge-community\slarge-community-list\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.large_community_large_community_list", @@ -1099,8 +1062,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_metric", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\smetric\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\smetric\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.metric", @@ -1127,8 +1089,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_origin", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sorigin\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sorigin\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.origin", @@ -1155,8 +1116,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_peer", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\speer\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\speer\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.peer", @@ -1185,8 +1145,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sip\saddress \s(?Paccess-list|prefix-list) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.address", @@ -1219,8 +1178,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sip\snexthop \s(?Paccess-list|prefix-list) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.next_hop", @@ -1253,8 +1211,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sip\sroute-source \s(?Paccess-list|prefix-list) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.route_source", @@ -1287,8 +1244,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sipv6\saddress \s(?Paccess-list|prefix-list) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ipv6.address", @@ -1320,8 +1276,7 @@ class Route_mapsTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sipv6\snexthop - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ipv6.next_hop", @@ -1349,8 +1304,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_protocol", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sprotocol\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sprotocol\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.protocol", @@ -1377,8 +1331,7 @@ class Route_mapsTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\srpki - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.rpki", diff --git a/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py b/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py index 6564280d..ea61a555 100644 --- a/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py @@ -33,8 +33,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "route_map", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "route_map", @@ -75,8 +74,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "call", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\scall\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\scall\s(?P\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} call {{call}}", @@ -99,8 +97,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "description", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sdescription\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sdescription\s(?P\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} description {{description}}", @@ -123,8 +120,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "action", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\saction\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\saction\s(?P\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} action {{action}}", @@ -147,8 +143,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "continue_sequence", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\scontinue\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\scontinue\s(?P\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} continue {{continue_sequence}}", @@ -198,8 +193,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "on_match_goto", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\son-match\sgoto\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\son-match\sgoto\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "on_match.goto", @@ -225,8 +219,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_aggregator_ip", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\saggregator\sip\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\saggregator\sip\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.aggregator.ip", @@ -254,8 +247,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_aggregator_as", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\saggregator\sas\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\saggregator\sas\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.aggregator.as", @@ -283,8 +275,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_as_path_exclude", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sas-path\sexclude\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sas-path\sexclude\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.as_path_exclude", @@ -364,8 +355,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_bgp_extcommunity_rt", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sbgp-extcommunity-rt\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sbgp-extcommunity-rt\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.bgp_extcommunity_rt", @@ -392,8 +382,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_comm_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\scommunity-list\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\scommunity-list\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.community.community_list", @@ -420,8 +409,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_comm_list_delete", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\scomm-list\sdelete(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\scomm-list\s(?Pdelete)\s*$""", re.VERBOSE, ), "compval": "set.comm_list.comm_list", @@ -448,8 +436,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_extcommunity_rt", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity\srt\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity\srt\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_rt", @@ -476,8 +463,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_extcommunity_soo", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity\ssoo\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity\ssoo\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_soo", @@ -504,8 +490,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_extcommunity_bandwidth", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity\sbandwidth\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sextcommunity\sbandwidth\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_bandwidth", @@ -560,8 +545,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_ip_next_hop", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sip-next-hop\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sip-next-hop\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.ip_next_hop", @@ -590,8 +574,7 @@ class Route_mapsTemplate14(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sipv6-next-hop \s(?Pglobal|local) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.ipv6_next_hop", @@ -649,8 +632,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_local_preference", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\slocal-preference\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\slocal-preference\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.local_preference", @@ -677,8 +659,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_metric", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\smetric\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\smetric\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.metric", @@ -705,8 +686,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_metric_type", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\smetric-type\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\smetric-type\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.metric_type", @@ -733,8 +713,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_origin", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sorigin\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sorigin\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.origin", @@ -761,8 +740,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_originator_id", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\soriginator-id\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\soriginator-id\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.originator_id", @@ -789,8 +767,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_src", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\ssrc\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\ssrc\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.src", @@ -817,8 +794,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_tag", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\stag\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\stag\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.tag", @@ -845,8 +821,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_weight", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sweight\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\sweight\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "set.weight", @@ -873,8 +848,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "set_table", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\stable\s(?P
\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\sset\stable\s(?P
\S+)\s*$""", re.VERBOSE, ), "compval": "set.weight", @@ -931,8 +905,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "match_as_path", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sas-path\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sas-path\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.as_path", @@ -959,8 +932,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "match_community_community_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\scommunity-list\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\scommunity\scommunity-list\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.community.community_list", @@ -1015,8 +987,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "match_extcommunity", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sextcommunity\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sextcommunity\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.extcommunity", @@ -1043,8 +1014,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "match_interface", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sinterface\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sinterface\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.interface", @@ -1071,8 +1041,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "match_large_community_large_community_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\slarge-community\slarge-community-list\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\slarge-community\slarge-community-list\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.large_community_large_community_list", @@ -1099,8 +1068,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "match_metric", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\smetric\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\smetric\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.metric", @@ -1127,8 +1095,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "match_origin", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sorigin\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sorigin\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.origin", @@ -1155,8 +1122,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "match_peer", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\speer\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\speer\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.peer", @@ -1185,8 +1151,7 @@ class Route_mapsTemplate14(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sip\saddress \s(?Paccess-list|prefix-list) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.address", @@ -1219,8 +1184,7 @@ class Route_mapsTemplate14(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sip\snexthop \s(?Paccess-list|prefix-list) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.next_hop", @@ -1253,8 +1217,7 @@ class Route_mapsTemplate14(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sip\sroute-source \s(?Paccess-list|prefix-list) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.route_source", @@ -1287,8 +1250,7 @@ class Route_mapsTemplate14(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sipv6\saddress \s(?Paccess-list|prefix-list) - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ipv6.address", @@ -1320,8 +1282,7 @@ class Route_mapsTemplate14(NetworkTemplate): "getval": re.compile( r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sipv6\snexthop - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.ipv6.next_hop", @@ -1349,8 +1310,7 @@ class Route_mapsTemplate14(NetworkTemplate): "name": "match_protocol", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sprotocol\s(?P\S+) - *$""", + ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\sprotocol\s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.protocol", @@ -1377,8 +1337,7 @@ class Route_mapsTemplate14(NetworkTemplate): "getval": re.compile( r""" ^set\spolicy\sroute-map\s(?P\S+)\srule\s(?P\d+)\smatch\srpki - \s(?P\S+) - *$""", + \s(?P\S+)\s*$""", re.VERBOSE, ), "compval": "match.rpki", diff --git a/plugins/module_utils/network/vyos/rm_templates/snmp_server.py b/plugins/module_utils/network/vyos/rm_templates/snmp_server.py index 71753083..ff1c6465 100644 --- a/plugins/module_utils/network/vyos/rm_templates/snmp_server.py +++ b/plugins/module_utils/network/vyos/rm_templates/snmp_server.py @@ -140,9 +140,9 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\scommunity \s+(?P\S+) - \s*(?Pauthorization\srw|authorization\sro)* - \s*(client\s(?P\S+))* - \s*(network\s(?P\S+))* + \s*(?Pauthorization\srw|authorization\sro)? + \s*(client\s(?P\S+))? + \s*(network\s(?P\S+))? $""", re.VERBOSE, ), @@ -164,8 +164,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\scontact - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp contact {{ contact }}", @@ -179,8 +178,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\sdescription - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp description {{ description }}", @@ -195,8 +193,8 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\slisten-address \s+(?P\S+) - \s*(port)* - \s*(?P\d+)* + \s*(port)? + \s*(?P\d+)? $""", re.VERBOSE, ), @@ -232,8 +230,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\ssmux-peer - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp smux-peer {{ smux_peer }}", @@ -247,8 +244,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\strap-source - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp trap-source {{ trap_source }}", @@ -263,9 +259,8 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\strap-target \s+(?P\S+) - \s*(?Pcommunity\s\S+)* - \s*(?Pport\s\d+)* - $""", + \s*(?Pcommunity\s\S+)? + \s*(?Pport\s\d+)? $""", re.VERBOSE, ), "setval": _tmplt_snmp_server_trap_target, @@ -283,8 +278,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\sv3\sengineid - \s+(?P\S+) - *$""", + \s+(?P\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp v3 engineid {{ snmp_v3.engine_id }}", @@ -301,9 +295,9 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\sgroup \s+(?P\S+) - \s*(?Pmode\s\S+)* - \s*(?Pseclevel\s\S+)* - \s*(?Pview\s\S+)* + \s*(?Pmode\s\S+)? + \s*(?Pseclevel\s\S+)? + \s*(?Pview\s\S+)? $""", re.VERBOSE, ), @@ -329,9 +323,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\strap-target \s+(?P\S+) \s+auth - \s*(?Pencrypted-password\s\S+)* - \s*(?Pplaintext-password\s\S+)* - \s*(?Ptype\s\S+)* + \s*(?Pencrypted-password\s\S+)? + \s*(?Pplaintext-password\s\S+)? + \s*(?Ptype\s\S+)? $""", re.VERBOSE, ), @@ -358,8 +352,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P\S+) - \s+(?Pport\s\d+)* - $""", + \s+(?Pport\s\d+)? $""", re.VERBOSE, ), "setval": "service snmp v3 trap-target port {{ snmp_v3.trap_targets.port }}", @@ -381,7 +374,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P\S+) - \s+(?Pprotocol\s\S+)* + \s+(?Pprotocol\s\S+)? $""", re.VERBOSE, ), @@ -404,7 +397,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P\S+) - \s+(?Ptype\s\S+)* + \s+(?Ptype\s\S+)? $""", re.VERBOSE, ), @@ -427,7 +420,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P\S+) - \s+(?Puser\s\S+)* + \s+(?Puser\s\S+)? $""", re.VERBOSE, ), @@ -451,9 +444,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\strap-target \s+(?P\S+) \s+privacy - \s*(?Pencrypted-password\s\S+)* - \s*(?Pplaintext-password\s\S+)* - \s*(?Ptype\s\S+)* + \s*(?Pencrypted-password\s\S+)? + \s*(?Pplaintext-password\s\S+)? + \s*(?Ptype\s\S+)? $""", re.VERBOSE, ), @@ -481,9 +474,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\suser \s+(?P\S+) \s+auth - \s*(?Pencrypted-password\s\S+)* - \s*(?Pplaintext-password\s\S+)* - \s*(?Ptype\s\S+)* + \s*(?Pencrypted-password\s\S+)? + \s*(?Pplaintext-password\s\S+)? + \s*(?Ptype\s\S+)? $""", re.VERBOSE, ), @@ -511,9 +504,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\suser \s+(?P\S+) \s+privacy - \s*(?Pencrypted-password\s\S+)* - \s*(?Pplaintext-password\s\S+)* - \s*(?Ptype\s\S+)* + \s*(?Pencrypted-password\s\S+)? + \s*(?Pplaintext-password\s\S+)? + \s*(?Ptype\s\S+)? $""", re.VERBOSE, ), @@ -540,8 +533,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\suser \s+(?P\S+) - \s+(?Pgroup\s.+)* - $""", + \s+(?Pgroup\s.+)? $""", re.VERBOSE, ), "setval": "service snmp v3 user {{ snmp_v3.users.user }} group {{ snmp_v3.users.group }}", @@ -563,7 +555,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\suser \s+(?P\S+) - \s+(?Pmode\s\S+)* + \s+(?Pmode\s\S+)? $""", re.VERBOSE, ), @@ -587,8 +579,8 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\sview \s+(?P\S+) \s+(?Poid\s\S+) - \s*(?Pexclude\s\S+)* - \s*(?Pmask\s\S+)* + \s*(?Pexclude\s\S+)? + \s*(?Pmask\s\S+)? $""", re.VERBOSE, ), diff --git a/tests/unit/modules/network/vyos/test_rm_templates_perf.py b/tests/unit/modules/network/vyos/test_rm_templates_perf.py new file mode 100644 index 00000000..d2060f46 --- /dev/null +++ b/tests/unit/modules/network/vyos/test_rm_templates_perf.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""Performance budget tests for rm_templates parsers. + +These tests guard against re-introducing catastrophic regex backtracking +in the rm_template parsers (T8609). Pre-fix, parse() over realistic +device-output input could take 50+ seconds because of `(group)*` +quantifiers on groups containing `\\S+`. Post-fix, the same input +parses in single-digit milliseconds. + +A 1-second budget is comfortably above post-fix runtime and well below +the pre-regression cliff, so the test fails sharply if the bug returns. +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import time + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_address_family_14 import ( + Bgp_address_familyTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_global_14 import ( + Bgp_globalTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps_14 import ( + Route_mapsTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.snmp_server import ( + Snmp_serverTemplate, +) + + +PARSE_BUDGET_SECONDS = 1.0 + + +def _time_parse(parser_class, lines): + """Time a single ``parse()`` call against ``lines``; returns elapsed seconds.""" + parser = parser_class(lines=lines) + t0 = time.perf_counter() + parser.parse() + return time.perf_counter() - t0 + + +def test_route_maps_14_parse_budget(): + """Realistic route-map config: parse() must finish under 1s. + + Pre-T8609: ~50s. Post-fix: <50ms. Inputs use 20+ char names + because the backtracking is exponential in the first \\S+ run + after the prefix. + """ + lines = [ + "set policy route-map ADVERTISE-ANYCAST-v6 rule 10 action 'permit'", + "set policy route-map ADVERTISE-ANYCAST-v6 rule 10 match ipv6 address prefix-list 'ANYCAST-AGGREGATE-v6'", + "set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 action 'permit'", + "set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 match ipv6 address prefix-list 'AS64496-SENTINEL-v6'", + "set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 set local-preference '120'", + "set policy route-map IXP-PEER-INGRESS-v4 rule 10 action 'permit'", + "set policy route-map IXP-PEER-INGRESS-v4 rule 10 match ip address prefix-list 'IXP-INBOUND-v4'", + "set policy route-map IXP-PEER-INGRESS-v4 rule 10 set community 'additive 65000:100'", + "set policy route-map TRANSIT-EGRESS-v4 rule 100 action 'permit'", + "set policy route-map TRANSIT-EGRESS-v4 rule 100 match ip address prefix-list 'CUSTOMER-PREFIXES-v4'", + "set policy route-map TRANSIT-EGRESS-v4 rule 100 set as-path prepend '65000 65000'", + "set policy route-map UPSTREAM-INGRESS-v4 rule 10 action 'permit'", + ] + elapsed = _time_parse(Route_mapsTemplate14, lines) + assert elapsed < PARSE_BUDGET_SECONDS, ( + "Route_mapsTemplate14.parse() took %.2fs (budget %.2fs); " + "possible regression of T8609 (rm_templates regex backtracking)." + % (elapsed, PARSE_BUDGET_SECONDS) + ) + + +def test_bgp_global_14_parse_budget(): + """Realistic BGP neighbor/address-family config: parse() under 1s.""" + lines = [ + "set protocols bgp 65001 neighbor 2001:db8:abcd:1234::1 remote-as '65002'", + "set protocols bgp 65001 neighbor 2001:db8:abcd:1234::1 description 'IXP-PEER-1'", + "set protocols bgp 65001 neighbor 2001:db8:abcd:1234::1 address-family ipv6-unicast route-map import 'IXP-INGRESS-v6'", + "set protocols bgp 65001 neighbor 2001:db8:abcd:1234::1 address-family ipv6-unicast route-map export 'IXP-EGRESS-v6'", + "set protocols bgp 65001 neighbor 192.0.2.1 remote-as '65003'", + "set protocols bgp 65001 neighbor 192.0.2.1 description 'TRANSIT-PROVIDER-1'", + "set protocols bgp 65001 neighbor 192.0.2.1 address-family ipv4-unicast route-map import 'TRANSIT-INGRESS-v4'", + "set protocols bgp 65001 neighbor 192.0.2.1 address-family ipv4-unicast route-map export 'TRANSIT-EGRESS-v4'", + ] + elapsed = _time_parse(Bgp_globalTemplate14, lines) + assert elapsed < PARSE_BUDGET_SECONDS, ( + "Bgp_globalTemplate14.parse() took %.2fs (budget %.2fs); " + "possible regression of T8609 (rm_templates regex backtracking)." + % (elapsed, PARSE_BUDGET_SECONDS) + ) + + +def test_snmp_server_parse_budget(): + """Realistic SNMP v3 config: parse() under 1s.""" + lines = [ + "set service snmp community PUBLIC-COMMUNITY-NAME-1 authorization 'ro'", + "set service snmp community PUBLIC-COMMUNITY-NAME-1 client '192.0.2.0/24'", + "set service snmp v3 trap-target TRAP-TARGET-LONG-NAME-1 user 'monitor'", + "set service snmp v3 trap-target TRAP-TARGET-LONG-NAME-1 protocol 'udp'", + "set service snmp v3 trap-target TRAP-TARGET-LONG-NAME-1 port '162'", + "set service snmp v3 user TRAP-USER-LONG-NAME-1 mode 'auth'", + "set service snmp v3 user TRAP-USER-LONG-NAME-1 group 'monitor'", + ] + elapsed = _time_parse(Snmp_serverTemplate, lines) + assert elapsed < PARSE_BUDGET_SECONDS, ( + "Snmp_serverTemplate.parse() took %.2fs (budget %.2fs); " + "possible regression of T8609 (rm_templates regex backtracking)." + % (elapsed, PARSE_BUDGET_SECONDS) + ) + + +def test_bgp_address_family_14_parse_budget(): + """Realistic BGP address-family aggregate config: parse() under 1s.""" + lines = [ + "set protocols bgp 65001 address-family ipv4-unicast network 198.51.100.0/24 backdoor", + "set protocols bgp 65001 address-family ipv4-unicast network 198.51.100.0/24 path-limit '4'", + "set protocols bgp 65001 address-family ipv4-unicast network 198.51.100.0/24 route-map 'NET-IN-v4'", + "set protocols bgp 65001 address-family ipv4-unicast aggregate-address 203.0.113.0/24 as-set", + "set protocols bgp 65001 address-family ipv4-unicast aggregate-address 203.0.113.0/24 summary-only", + "set protocols bgp 65001 address-family ipv6-unicast network 2001:db8:abcd:1234::/64 backdoor", + "set protocols bgp 65001 address-family ipv6-unicast network 2001:db8:abcd:1234::/64 route-map 'NET-IN-v6'", + "set protocols bgp 65001 address-family ipv6-unicast aggregate-address 2001:db8::/32 as-set", + "set protocols bgp 65001 address-family ipv6-unicast aggregate-address 2001:db8::/32 summary-only", + ] + elapsed = _time_parse(Bgp_address_familyTemplate14, lines) + assert elapsed < PARSE_BUDGET_SECONDS, ( + "Bgp_address_familyTemplate14.parse() took %.2fs (budget %.2fs); " + "possible regression of T8609 (rm_templates regex backtracking)." + % (elapsed, PARSE_BUDGET_SECONDS) + ) + + +def test_route_maps_14_set_comm_list_delete_matches_setval(): + """Round-trip check: the `set_comm_list_delete` parser must match the line its setval generates. + + `set_comm_list_delete`'s setval emits `set policy route-map X rule N set + comm-list delete` with no token after `delete`. Pre-T8609 the getval + happened to match this by accident (a `*` quantifier on the trailing + `(?P\\S+)` made the group optional after VERBOSE-strip). An + earlier draft of T8609's fix made the group required, causing the + parser to silently ignore its own output. This test guards the + round-trip. + """ + line = "set policy route-map MY-MAP rule 10 set comm-list delete" + parser = Route_mapsTemplate14(lines=[line]) + result = parser.parse() + rm = result.get("route_maps", {}).get("MY-MAP") + assert rm is not None, ( + "route_maps_14: set_comm_list_delete parser failed to match its " + "own setval-generated line %r; the parser is broken." % line + ) + entry = rm.get("entries", {}).get(10, {}) + comm_list = entry.get("set", {}).get("comm_list", {}) + assert comm_list.get("delete"), ( + "route_maps_14: set_comm_list_delete matched the line but did not " + "populate set.comm_list.delete; check the result template." + ) -- cgit v1.2.3 From 6d7a2184e970624f74f333931f7f70a1f900f7b5 Mon Sep 17 00:00:00 2001 From: Stavros Kroustouris Date: Thu, 25 Jun 2026 22:30:57 +0300 Subject: T2295: vyos_user: quote plaintext-password in generated set commands (#480) * vyos_user: quote plaintext-password in generated set commands VyOS requires quoted values for passwords with special characters. Align with encrypted-password and integration test conventions. Co-authored-by: Cursor * T2295: escape plaintext-password values in vyos_user set commands Quote passwords for VyOS special-character handling and escape embedded single quotes. Update RETURN sample, add unit tests, refresh changelog. Co-authored-by: Cursor * T2295: shorten changelog fragment for ansible-lint line-length Co-authored-by: Cursor * T2295: use shlex.quote() for plaintext-password values Replace custom _quote_config_value() with stdlib shlex.quote() per review feedback; update unit tests and RETURN sample accordingly. Co-authored-by: Cursor * T2295: add complex plaintext-password quoting unit test Cover spaces, shell metacharacters, embedded quotes, and backslashes in one password; assert command output matches shlex.quote(). Co-authored-by: Cursor * T2295: assert explicit quoting in complex password unit test Replace shlex.quote()-derived expectation with a fixed command string, matching the other password quoting tests. Co-authored-by: Cursor * Fix RETURN doc sample, add docstrings and workflow permissions - RETURN sample was missing the username token in the example command - Add docstrings to all undocumented module-level functions to bring docstring coverage above the 80% threshold - Add explicit permissions: contents: read to codecoverage.yml Co-Authored-By: Claude Sonnet 4.6 * Revert "Fix RETURN doc sample, add docstrings and workflow permissions" This reverts commit 827809ee49cf472a8bf30ec0b24d347a75fcb083. --------- Co-authored-by: Cursor Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .../vyos-user-quote-plaintext-password.yml | 3 +++ plugins/modules/vyos_user.py | 4 ++- tests/unit/modules/network/vyos/test_vyos_user.py | 30 ++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/vyos-user-quote-plaintext-password.yml diff --git a/changelogs/fragments/vyos-user-quote-plaintext-password.yml b/changelogs/fragments/vyos-user-quote-plaintext-password.yml new file mode 100644 index 00000000..487b592a --- /dev/null +++ b/changelogs/fragments/vyos-user-quote-plaintext-password.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - vyos_user - Quote and escape plaintext-password values in set commands so VyOS accepts special characters and embedded single quotes. diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py index d2f23509..e47f5a05 100644 --- a/plugins/modules/vyos_user.py +++ b/plugins/modules/vyos_user.py @@ -205,6 +205,7 @@ commands: """ import re +import shlex from copy import deepcopy from functools import partial @@ -276,7 +277,8 @@ def spec_to_commands(updates, module): add( commands, want, - "authentication plaintext-password %s" % want["configured_password"], + "authentication plaintext-password %s" + % shlex.quote(want["configured_password"]), ) return commands diff --git a/tests/unit/modules/network/vyos/test_vyos_user.py b/tests/unit/modules/network/vyos/test_vyos_user.py index d1e7f162..2cbd3820 100644 --- a/tests/unit/modules/network/vyos/test_vyos_user.py +++ b/tests/unit/modules/network/vyos/test_vyos_user.py @@ -62,6 +62,36 @@ class TestVyosUserModule(TestVyosModule): ["set system login user ansible authentication plaintext-password test"], ) + def test_vyos_user_password_special_chars(self): + set_module_args(dict(name="ansible", configured_password="test$123!@")) + result = self.execute_module(changed=True) + self.assertEqual( + result["commands"], + [ + "set system login user ansible authentication plaintext-password 'test$123!@'", + ], + ) + + def test_vyos_user_password_embedded_quote(self): + set_module_args(dict(name="ansible", configured_password="pa'ss")) + result = self.execute_module(changed=True) + self.assertEqual( + result["commands"], + [ + "set system login user ansible authentication plaintext-password 'pa'\"'\"'ss'", + ], + ) + + def test_vyos_user_password_complex_special_chars(self): + set_module_args(dict(name="ansible", configured_password="P@ss w0rd!$#'xy\\")) + result = self.execute_module(changed=True) + self.assertEqual( + result["commands"], + [ + "set system login user ansible authentication plaintext-password 'P@ss w0rd!$#'\"'\"'xy\\'", + ], + ) + def test_vyos_user_delete(self): set_module_args(dict(name="ansible", state="absent")) result = self.execute_module(changed=True) -- cgit v1.2.3 From 0e10d136ef280ca147b6f1e332072f6ba14c8844 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Tue, 30 Jun 2026 14:18:50 +0300 Subject: T8522: fix parse_icmp_attr() split delimiter and UnboundLocalError (#464) * Add easy-wins improvement spec for vyos.vyos collection Covers five phases: formatting compliance, runtime.yml redirect fix, deprecated feature cleanup, missing unit tests, and template deduplication. Co-Authored-By: Claude Opus 4.6 (1M context) * Add implementation plan for easy-wins improvements 15 tasks across 5 phases: formatting compliance, runtime.yml bugfix, deprecated code cleanup, missing unit tests, template deduplication. Co-Authored-By: Claude Opus 4.6 (1M context) * Update spec and plan with architect review corrections Key changes: - Defer Phase 5 (template deduplication) to v7.0.0: route_maps are not identical, BGP dedup blocked by Python module-level scoping, OSPF has fundamentally different command paradigms - Add .git-blame-ignore-revs step to Phase 1 - Add missing test cases: overridden/rendered for resource modules, aggregate/purge/with_address for vyos_vlan - Fix incorrect claim that version.py LooseVersion is unused - Add sequential merge requirement to preamble Co-Authored-By: Claude Opus 4.6 (1M context) * chore: add .worktrees/ and .claude/ to .gitignore * T8522: fix parse_icmp_attr() split delimiter and UnboundLocalError - Use val.split("/") instead of val.split(".") when parsing type/code pairs - In the numeric-only branch, use int(val) instead of the undefined type_no variable - Cast both type and code to int for consistent typing Co-Authored-By: Claude Sonnet 4.6 * T8522: remove unrelated planning docs and gitignore changes These files were included from local main branch commits unrelated to this bugfix. This commit removes them to keep the PR scoped. Co-Authored-By: Claude Sonnet 4.6 * T8522: add parsed test for legacy type/code ICMP format Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- changelogs/fragments/fix-icmp-parse-attr.yml | 5 +++++ .../network/vyos/facts/firewall_rules/firewall_rules.py | 8 ++++---- .../modules/network/vyos/test_vyos_firewall_rules13.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/fix-icmp-parse-attr.yml diff --git a/changelogs/fragments/fix-icmp-parse-attr.yml b/changelogs/fragments/fix-icmp-parse-attr.yml new file mode 100644 index 00000000..5cf0e199 --- /dev/null +++ b/changelogs/fragments/fix-icmp-parse-attr.yml @@ -0,0 +1,5 @@ +--- +bugfixes: + - firewall_rules - fix parse_icmp_attr() using wrong split delimiter ('.' instead of '/') + and referencing undefined variable type_no in the numeric-only branch, which caused + ValueError or UnboundLocalError when gathering firewall rules with ICMP type conditions. diff --git a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py index 270b4a62..f8f02e7a 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py @@ -412,11 +412,11 @@ class Firewall_rulesFacts(object): # <1.3 could be # (type), #/# (type/code) or 'type' (type_name) # recent this is only for strings if "/" in val: # type/code - (type_no, code) = val.split(".") - config["type"] = type_no - config["code"] = code + (type_no, code) = val.split("/") + config["type"] = int(type_no) + config["code"] = int(code) elif val.isnumeric(): - config["type"] = type_no + config["type"] = int(val) else: config["type_name"] = val return config diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py b/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py index 1af61752..edce123b 100644 --- a/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py @@ -1576,3 +1576,17 @@ class TestVyosFirewallRulesModule13(TestVyosModule): ] self.maxDiff = None self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_rules_parsed_icmp_type_code(self): + """parse_icmp_attr: legacy 'type/code' token parses into integer type and code.""" + raw = ( + "set firewall name TEST rule 1 action 'accept'\n" + "set firewall name TEST rule 1 protocol 'icmp'\n" + "set firewall name TEST rule 1 icmp type '3/4'\n" + ) + set_module_args(dict(running_config=raw, state="parsed")) + result = self.execute_module(changed=False) + parsed = result["parsed"] + rule = parsed[0]["rule_sets"][0]["rules"][0] + self.assertEqual(rule["icmp"]["type"], 3) + self.assertEqual(rule["icmp"]["code"], 4) -- cgit v1.2.3 From e4c9d5d2aa845f8b738be2fa1cbc60b9f0fa8805 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Wed, 1 Jul 2026 14:33:16 +0300 Subject: T8513: add .git-blame-ignore-revs to exclude bulk formatting commits (#455) * T8513: add .git-blame-ignore-revs to exclude bulk formatting commits * T8513: fix changelog fragment - use trivial section and correct description Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: omnom62 <75066712+omnom62@users.noreply.github.com> --- .git-blame-ignore-revs | 4 ++++ changelogs/fragments/formatting-compliance.yml | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 .git-blame-ignore-revs create mode 100644 changelogs/fragments/formatting-compliance.yml diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..dedbdc55 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# Formatting commits - ignore in git blame +# style: apply black formatting across entire codebase +# NOTE: Update this hash after the formatting PR is merged +221ac461e4b3dcf37629f4cce89e45a59ace873c diff --git a/changelogs/fragments/formatting-compliance.yml b/changelogs/fragments/formatting-compliance.yml new file mode 100644 index 00000000..d48896a9 --- /dev/null +++ b/changelogs/fragments/formatting-compliance.yml @@ -0,0 +1,3 @@ +--- +trivial: + - Add .git-blame-ignore-revs to exclude bulk formatting commits from git blame output. -- cgit v1.2.3 From 17041b6f6953b993762854a67953fb35176b46e7 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Tue, 14 Jul 2026 22:42:23 +0300 Subject: ci: T9082: onboard CodeQL scanning (#488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: T9082: onboard CodeQL scanning via central reusable workflow 🤖 Generated by [robots](https://vyos.io) * ci: T9082: add trivial changelog fragment for the CodeQL workflow 🤖 Generated by [robots](https://vyos.io) --- .github/workflows/codeql.yml | 30 ++++++++++++++++++++++++++ changelogs/fragments/T9082-codeql-workflow.yml | 2 ++ 2 files changed, 32 insertions(+) create mode 100644 .github/workflows/codeql.yml create mode 100644 changelogs/fragments/T9082-codeql-workflow.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..b83b94e0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,30 @@ +name: "Perform CodeQL Analysis" + +on: + push: + branches: + - main + paths: + - '**' + - '!.github/**' + - '!**/*.md' + pull_request: + branches: + - main + paths: + - '**' + - '!.github/**' + - '!**/*.md' + schedule: + - cron: '52 13 * * 0' + +permissions: + actions: read + contents: read + security-events: write + +jobs: + codeql-analysis-call: + uses: vyos/.github/.github/workflows/codeql-analysis.yml@production + with: + languages: "['python']" diff --git a/changelogs/fragments/T9082-codeql-workflow.yml b/changelogs/fragments/T9082-codeql-workflow.yml new file mode 100644 index 00000000..5d09acd7 --- /dev/null +++ b/changelogs/fragments/T9082-codeql-workflow.yml @@ -0,0 +1,2 @@ +trivial: + - Add CodeQL analysis workflow calling the VyOS org-wide central reusable (T9082). -- cgit v1.2.3