diff --git a/.coderabbit.yaml b/.coderabbit.yaml index b23fb30b..2b343aa6 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,591 +1,205 @@ # 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/**" - - "!changelogs/changelog.yaml" - - "!.venv/**" - - "!.collections/**" - - "!.worktrees/**" + # 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' + - '!.collections/**' 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. + 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/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. 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/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). diff --git a/plugins/modules/vyos_config.py b/plugins/modules/vyos_config.py index 20b63729..e3817ba5 100644 --- a/plugins/modules/vyos_config.py +++ b/plugins/modules/vyos_config.py @@ -1,421 +1,448 @@ #!/usr/bin/python # # 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 DOCUMENTATION = """ module: vyos_config author: Nathaniel Case (@Qalthos) short_description: Manage VyOS configuration on remote device description: - This module provides configuration file management of VyOS devices. It provides arguments for managing both the configuration file and state of the active configuration. All configuration statements are based on `set` and `delete` commands in the device configuration. version_added: 1.0.0 extends_documentation_fragment: - vyos.vyos.vyos notes: - Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). - To ensure idempotency and correct diff the configuration lines in the relevant module options should be similar to how they appear if present in the running configuration on device including the indentation. options: lines: description: - The ordered set of commands that should be configured in the section. The commands must be the exact same commands as found in the device running-config as found in the device running-config to ensure idempotency and correct diff. Be sure to note the configuration command syntax as some commands are automatically modified by the device config parser. type: list elements: str src: description: - The C(src) argument specifies the path to the source config file to load. The source config file can either be in bracket format or set format. The source file can include Jinja2 template variables. The configuration lines in the source file should be similar to how it will appear if present in the running-configuration of the device including indentation to ensure idempotency and correct diff. type: path match: description: - The C(match) argument controls the method used to match against the current active configuration. By default, the desired config is matched against the active config and the deltas are loaded. If the C(match) argument is set to C(none) the active configuration is ignored and the configuration is always loaded. type: str default: line choices: - line - smart - none backup: description: - The C(backup) argument will backup the current devices active configuration to the Ansible control host prior to making any changes. If the C(backup_options) value is not given, the backup file will be located in the backup folder in the playbook root directory or role root directory, if playbook is part of an ansible role. If the directory does not exist, it is created. type: bool default: no comment: description: - Allows a commit description to be specified to be included when the configuration is committed. If the configuration is not changed or committed, this argument is ignored. default: configured by vyos_config type: str confirm: description: - The C(confirm) argument will tell vyos to revert to the previous configuration if not explicitly confirmed after applying the new config. When set to C(automatic) this module will automatically confirm the configuration, if the current session remains working with the new config. When set to C(manual), this module does not issue the confirmation itself. type: str default: none choices: - automatic - manual - none confirm_timeout: description: - Minutes to wait for confirmation before reverting the configuration. Does not apply when C(confirm) is set to C(none) . type: int default: 10 config: description: - The C(config) argument specifies the base configuration to use to compare against the desired configuration. If this value is not specified, the module will automatically retrieve the current active configuration from the remote device. The configuration lines in the option value should be similar to how it will appear if present in the running-configuration of the device including indentation to ensure idempotency and correct diff. type: str save: description: - The C(save) argument controls whether or not changes made to the active configuration are saved to disk. This is independent of committing the config. When set to True, the active configuration is saved. type: bool default: no backup_options: description: - This is a dict object containing configurable options related to backup file path. The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set to I(no) this option will be silently ignored. suboptions: filename: description: - The filename to be used to store the backup configuration. If the filename is not given it will be generated based on the hostname, current time and date in format defined by _config.@ type: str dir_path: description: - This option provides the path ending with directory name in which the backup configuration file will be stored. If the directory does not exist it will be first created and the filename is either the value of C(filename) or default filename as described in C(filename) options description. If the path value is not given in that case a I(backup) directory will be created in the current working directory and backup configuration will be copied 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 = """ - name: configure the remote device vyos.vyos.vyos_config: lines: - set system host-name {{ inventory_hostname }} - set service lldp - delete service dhcp-server - name: backup and load from file vyos.vyos.vyos_config: src: vyos.cfg backup: true - name: render a Jinja2 template onto the VyOS router vyos.vyos.vyos_config: match: smart src: vyos_template.j2 - name: revert after ten minutes, if connection is lost vyos.vyos.vyos_config: src: vyos_template.j2 confirm: automatic - name: for idempotency, use full-form commands vyos.vyos.vyos_config: lines: # - set int eth eth2 description 'OUTSIDE' - set interface ethernet eth2 description 'OUTSIDE' - name: configurable backup path vyos.vyos.vyos_config: backup: true backup_options: filename: backup.cfg dir_path: /home/user """ RETURN = """ commands: description: The list of configuration commands sent to the device returned: always type: list sample: ['...', '...'] filtered: description: The list of configuration commands removed to avoid a load failure returned: always type: list sample: ['...', '...'] backup_path: description: The full path to the backup file returned: when backup is yes type: str sample: /playbooks/ansible/backup/vyos_config.2016-07-16@22:28:34 filename: description: The name of the backup file returned: when backup is yes and filename is not specified in backup options type: str sample: vyos_config.2016-07-16@22:28:34 shortname: description: The full path to the backup file excluding the timestamp returned: when backup is yes and filename is not specified in backup options type: str sample: /playbooks/ansible/backup/vyos_config date: description: The date extracted from the backup file name returned: when backup is yes type: str sample: "2016-07-16" time: description: The time extracted from the backup file name returned: when backup is yes type: str sample: "22:28:34" """ import re from ansible.module_utils._text import to_text from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import ConnectionError from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( get_config, get_connection, load_config, run_commands, ) 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): contents = module.params["src"] or module.params["lines"] if module.params["src"]: contents = contents.splitlines() if len(contents) > 0: line = contents[0].split() if len(line) > 0 and line[0] in ("set", "delete"): contents = format_commands(contents) contents = "\n".join(contents) return contents def format_commands(commands): """ This function format the input commands and removes the prepend white spaces for command lines having 'set' or 'delete' and it skips empty lines. :param commands: :return: list of commands """ return [ line.strip() if line.split()[0] in ("set", "delete") else line for line in commands if len(line.strip()) > 0 ] def diff_config(commands, config): config = [str(c).replace("'", "") for c in config.splitlines()] updates = list() visited = set() for line in commands: item = str(line).replace("'", "") if not item.startswith("set") and not item.startswith("delete"): raise ValueError("line must start with either `set` or `delete`") elif item.startswith("set") and item not in config: updates.append(line) elif item.startswith("delete"): if not config: updates.append(line) else: item = re.sub(r"delete", "set", item) for entry in config: if entry.startswith(item) and line not in visited: updates.append(line) visited.add(line) 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] def run(module, result): # get the current active config from the node or passed in via # the config param config = module.params["config"] or get_config(module) # create the candidate config object from the arguments candidate = get_candidate(module) # create loadable config that includes only the configuration updates connection = get_connection(module) try: response = connection.get_diff( candidate=candidate, running=config, diff_match=module.params["match"], ) except ConnectionError as exc: 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 commit = not module.check_mode comment = module.params["comment"] confirm = None if module.params["confirm"] == "automatic" or module.params["confirm"] == "manual": confirm = module.params["confirm_timeout"] diff = None if commands: diff = load_config(module, commands, commit=commit, comment=comment, confirm=confirm) if module.params["confirm"] == "automatic": run_commands(module, ["configure", "confirm", "exit"]) if result.get("filtered"): result["warnings"].append( "Some configuration commands were removed, please see the filtered key", ) result["changed"] = True if module._diff: result["diff"] = {"prepared": diff} def main(): backup_spec = dict(filename=dict(), dir_path=dict(type="path")) argument_spec = dict( src=dict(type="path"), lines=dict(type="list", elements="str"), match=dict(default="line", choices=["line", "smart", "none"]), comment=dict(default=DEFAULT_COMMENT), confirm=dict(choices=["automatic", "manual", "none"], default='none'), confirm_timeout=dict(type="int", default=10), config=dict(), 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")] module = AnsibleModule( argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True, ) warnings = list() result = dict(changed=False, warnings=warnings) if module.params["backup"]: result["__backup__"] = get_config(module=module) if any((module.params["src"], module.params["lines"])): run(module, result) if module.params["save"]: diff = run_commands(module, commands=["configure", "compare saved"])[1] if diff not in { "[edit]", "No changes between working and saved configurations.\n\n[edit]", }: if not module.check_mode: run_commands(module, commands=["save"]) result["changed"] = True run_commands(module, commands=["exit"]) if result.get("changed") and any((module.params["src"], module.params["lines"])): msg = ( "To ensure idempotency and correct diff the input configuration lines should be" " similar to how they appear if present in" " the running configuration on device" ) if module.params["src"]: msg += " including the indentation" if "warnings" in result: result["warnings"].append(msg) else: result["warnings"] = msg module.exit_json(**result) if __name__ == "__main__": main() 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 @@ -1,57 +1,91 @@ --- - debug: msg="START cli/config_check.yaml on connection={{ ansible_connection }}" - name: setup- ensure interface is not present vyos.vyos.vyos_config: lines: delete interfaces loopback lo - name: setup- create interface register: result vyos.vyos.vyos_config: lines: - interfaces - interfaces loopback lo - interfaces loopback lo description test - name: Check that multiple duplicate lines collapse into a single commands assert: that: - result.commands|length == 1 - name: Check that set is correctly prepended assert: that: - result.commands[0] == 'set interfaces loopback lo description test' - name: configure config_check config command register: result vyos.vyos.vyos_config: lines: delete interfaces loopback lo - assert: that: - result.changed == true - name: check config_check config command idempontent register: result vyos.vyos.vyos_config: lines: delete interfaces loopback lo - assert: that: - result.changed == false - name: check multiple line config filter is working register: result vyos.vyos.vyos_config: 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 == 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 }}"