diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 38eae56f..c14e52eb 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/**' + # 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 ────────────────────────────────────────────── - 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/.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/.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 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 @@ -1,14 +1,14 @@ name: Refresh the automation hub token # the token expires every 30 days, so we need to refresh it on: schedule: - cron: '0 12 1,15 * *' # run 12pm on the 1st and 15th of the month workflow_dispatch: 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: ah_token: ${{ secrets.AH_TOKEN }} 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 @@ -1,15 +1,15 @@ name: "CLA Check" permissions: actions: write contents: read pull-requests: write statuses: write on: pull_request_target: types: [opened, synchronize, closed] issue_comment: 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 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/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/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. 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/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/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/meta/runtime.yml b/meta/runtime.yml index 36579e05..f0a53ee7 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,68 +1,68 @@ --- requires_ansible: ">=2.15.0" plugin_routing: modules: banner: redirect: vyos.vyos.vyos_banner bgp_global: redirect: vyos.vyos.vyos_bgp_global bgp_address_family: redirect: vyos.vyos.vyos_bgp_address_family command: redirect: vyos.vyos.vyos_command config: redirect: vyos.vyos.vyos_config facts: redirect: vyos.vyos.vyos_facts firewall_global: redirect: vyos.vyos.vyos_firewall_global firewall_interfaces: redirect: vyos.vyos.vyos_firewall_interfaces firewall_rules: redirect: vyos.vyos.vyos_firewall_rules hostname: redirect: vyos.vyos.vyos_hostname interfaces: redirect: vyos.vyos.vyos_interfaces l3_interfaces: redirect: vyos.vyos.vyos_l3_interfaces lag_interfaces: redirect: vyos.vyos.vyos_lag_interfaces lldp_global: redirect: vyos.vyos.vyos_lldp_global lldp_interfaces: redirect: vyos.vyos.vyos_lldp_interfaces logging: tombstone: removal_version: 6.0.0 warning_text: use vyos_logging_global instead vyos_logging: tombstone: removal_version: 6.0.0 warning_text: use vyos_logging_global instead logging_global: redirect: vyos.vyos.vyos_logging_global ntp_global: redirect: vyos.vyos.vyos_ntp_global ospfv2: redirect: vyos.vyos.vyos_ospfv2 ospfv3: redirect: vyos.vyos.vyos_ospfv3 ospf_interfaces: redirect: vyos.vyos.vyos_ospf_interfaces ping: redirect: vyos.vyos.vyos_ping 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: redirect: vyos.vyos.vyos_system user: redirect: vyos.vyos.vyos_user vlan: redirect: vyos.vyos.vyos_vlan vrf: redirect: vyos.vyos.vyos_vrf 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 @@ -1,1544 +1,1407 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Red Hat # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ The module file for vyos_bgp_global """ from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = """ module: vyos_bgp_global version_added: 1.0.0 short_description: BGP global resource module description: - This module manages BGP global configuration of interfaces on devices running VYOS. - Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 - The provided examples of commands are valid for VyOS 1.4+ author: - Gomathi Selvi Srinivasan (@GomathiselviS) options: config: description: A dict of BGP global configuration for interfaces. type: dict suboptions: as_number: description: - AS number. type: int # aggregate_address: # description: # - BGP aggregate network. # type: list # elements: dict # suboptions: # prefix: # description: # - BGP aggregate network. # type: str # as_set: # description: # - Generate AS-set path information for this aggregate address. # type: bool # summary_only: # description: # - Announce the aggregate summary network only. # type: bool #maximum_paths: --> moved to address-family before 1.3 # description: BGP multipaths # type: list # elements: dict # suboptions: # path: # description: BGP multipaths # type: str # count: # description: No. of paths. # type: int neighbor: description: BGP neighbor type: list elements: dict suboptions: address: description: - BGP neighbor address (v4/v6). type: str advertisement_interval: 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. type: dict suboptions: dynamic: description: - Advertise dynamic capability to this neighbor. type: bool extended_nexthop: 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 type: str description: description: - Description of the neighbor type: str disable_capability_negotiation: description: - Disbale capability negotiation with the neighbor type: bool disable_connected_check: description: - Disable check to see if EBGP peer's address is a connected route. type: bool disable_send_community: description: - 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 passive: description: Do not initiate a session with this neighbor type: bool password: description: BGP MD5 password type: str peer_group_name: description: IPv4 peer group for this peer type: str peer_group: description: True if all the configs under this neighbor key is for peer group template. type: bool 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 suboptions: connect: description: BGP connect timer for this neighbor. type: int holdtime: description: BGP hold timer for this neighbor type: int keepalive: description: BGP keepalive interval for this neighbor type: int ttl_security: description: Number of the maximum number of hops to the BGP peer type: int update_source: description: Source IP of routing updates type: str # network: # description: BGP network # type: list # elements: dict # suboptions: # address: # description: BGP network address # type: str # backdoor: # description: Network as a backdoor route # type: bool # route_map: # description: Route-map to modify route attributes # type: str # redistribute: # description: Redistribute routes from other protocols into BGP # type: list # elements: dict # suboptions: # protocol: # description: types of routes to be redistributed. # type: str # choices: ['connected', 'kernel', 'ospf', 'rip', 'static'] # route_map: # description: Route map to filter redistributed routes # type: str # metric: # description: Metric for redistributed routes. # type: int timers: description: BGP protocol timers type: dict suboptions: keepalive: description: Keepalive interval type: int holdtime: description: Hold time interval type: int bgp_params: description: BGP parameters type: dict suboptions: always_compare_med: description: Always compare MEDs from different neighbors type: bool bestpath: description: Default bestpath selection mechanism type: dict suboptions: as_path: description: AS-path attribute comparison parameters type: str choices: ['confed', 'ignore'] compare_routerid: description: Compare the router-id for identical EBGP paths type: bool med: description: MED attribute comparison parameters type: str choices: ['confed', 'missing-as-worst'] cluster_id: description: Route-reflector cluster-id type: str confederation: description: AS confederation parameters type: list elements: dict suboptions: identifier: description: Confederation AS identifier type: int peers: description: Peer ASs in the BGP confederation type: int dampening: description: Enable route-flap dampening type: dict suboptions: half_life: description: Half-life penalty in seconds type: int max_suppress_time: description: Maximum duration to suppress a stable route type: int re_use: description: Time to start reusing a route type: int start_suppress_time: description: When to start suppressing a route type: int default: description: BGP defaults type: dict suboptions: local_pref: description: Default local preference type: int no_ipv4_unicast: description: | Deactivate IPv4 unicast for a peer by default Deprecated: Unavailable after 1.4 type: bool deterministic_med: description: Compare MEDs between different peers in the same AS type: bool disable_network_import_check: description: Disable IGP route check for network statements type: bool distance: description: Administrative distances for BGP routes type: list elements: dict suboptions: type: description: Type of route type: str choices: ['external', 'internal', 'local'] value: description: distance type: int prefix: description: Administrative distance for a specific BGP prefix type: int enforce_first_as: description: Require first AS in the path to match peer's AS type: bool graceful_restart: description: Maximum time to hold onto restarting peer's stale paths type: int log_neighbor_changes: description: Log neighbor up/down changes and reset reason type: bool no_client_to_client_reflection: description: Disable client to client route reflection type: bool no_fast_external_failover: description: Disable immediate session reset if peer's connected link goes down type: bool router_id: description: BGP router-id type: str scan_time: description: BGP route scanner interval type: int state: description: - The state the configuration should be left in. - State I(purged) removes all the BGP configurations from the target device. Use caution with this state.('delete protocols bgp ') - State I(deleted) only removes BGP attributes that this modules manages and does not negate the BGP process completely. Thereby, preserving address-family related configurations under BGP context. - Running states I(deleted) and I(replaced) will result in an error if there are address-family configuration lines present under neighbor context that is is to be removed. Please use the M(vyos.vyos.vyos_bgp_address_family) module for prior cleanup. - Refer to examples for more details. type: str choices: [deleted, merged, purged, replaced, gathered, rendered, parsed] default: merged running_config: description: - This option is used only with state I(parsed). - The value of this option should be the output received from the EOS device by executing the command B(show running-config | section bgp). - The state I(parsed) reads the configuration from C(running_config) option and transforms it into Ansible structured data as per the resource module's argspec and the value is then returned in the I(parsed) key within the result. type: str """ EXAMPLES = """ # Using merged # Before state # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # vyos@vyos:~$ - name: Merge provided configuration with device configuration vyos.vyos.vyos_bgp_global: config: as_number: "65536" aggregate_address: - prefix: "203.0.113.0/24" as_set: true - prefix: "192.0.2.0/24" summary_only: true network: - address: "192.1.13.0/24" backdoor: true redistribute: - protocol: "kernel" metric: 45 - protocol: "connected" route_map: "map01" maximum_paths: - path: "ebgp" count: 20 - path: "ibgp" count: 55 timers: keepalive: 35 bgp_params: bestpath: as_path: "confed" compare_routerid: true default: no_ipv4_unicast: true router_id: "192.1.2.9" confederation: - peers: 20 - peers: 55 - identifier: 66 neighbor: - address: "192.0.2.25" disable_connected_check: true timers: holdtime: 30 keepalive: 10 - address: "203.0.113.5" attribute_unchanged: as_path: true med: true ebgp_multihop: 2 remote_as: 101 update_source: "192.0.2.25" - address: "5001::64" maximum_prefix: 34 distribute_list: - acl: 20 action: "export" - acl: 40 action: "import" state: merged # After State # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # set protocols bgp system-as 65536 # set protocols bgp aggregate-address 192.0.2.0/24 'summary-only' # set protocols bgp aggregate-address 203.0.113.0/24 'as-set' # set protocols bgp maximum-paths ebgp '20' # set protocols bgp maximum-paths ibgp '55' # set protocols bgp neighbor 192.0.2.25 'disable-connected-check' # set protocols bgp neighbor 192.0.2.25 timers holdtime '30' # set protocols bgp neighbor 192.0.2.25 timers keepalive '10' # set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'as-path' # set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'med' # set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'next-hop' # set protocols bgp neighbor 203.0.113.5 ebgp-multihop '2' # set protocols bgp neighbor 203.0.113.5 remote-as '101' # set protocols bgp neighbor 203.0.113.5 update-source '192.0.2.25' # set protocols bgp neighbor 5001::64 distribute-list export '20' # set protocols bgp neighbor 5001::64 distribute-list import '40' # set protocols bgp neighbor 5001::64 maximum-prefix '34' # set protocols bgp network 192.1.13.0/24 'backdoor' # set protocols bgp parameters bestpath as-path 'confed' # set protocols bgp parameters bestpath 'compare-routerid' # set protocols bgp parameters confederation identifier '66' # set protocols bgp parameters confederation peers '20' # set protocols bgp parameters confederation peers '55' # set protocols bgp parameters default 'no-ipv4-unicast' # set protocols bgp parameters router-id '192.1.2.9' # set protocols bgp redistribute connected route-map 'map01' # set protocols bgp redistribute kernel metric '45' # set protocols bgp timers keepalive '35' # vyos@vyos:~$ # # # Module Execution: # # "after": { # "aggregate_address": [ # { # "prefix": "192.0.2.0/24", # "summary_only": true # }, # { # "prefix": "203.0.113.0/24", # "as_set": true # } # ], # "as_number": 65536, # "bgp_params": { # "bestpath": { # "as_path": "confed", # "compare_routerid": true # }, # "confederation": [ # { # "identifier": 66 # }, # { # "peers": 20 # }, # { # "peers": 55 # } # ], # "default": { # "no_ipv4_unicast": true # }, # "router_id": "192.1.2.9" # }, # "maximum_paths": [ # { # "count": 20, # "path": "ebgp" # }, # { # "count": 55, # "path": "ibgp" # } # ], # "neighbor": [ # { # "address": "192.0.2.25", # "disable_connected_check": true, # "timers": { # "holdtime": 30, # "keepalive": 10 # } # }, # { # "address": "203.0.113.5", # "attribute_unchanged": { # "as_path": true, # "med": true, # "next_hop": true # }, # "ebgp_multihop": 2, # "remote_as": 101, # "update_source": "192.0.2.25" # }, # { # "address": "5001::64", # "distribute_list": [ # { # "acl": 20, # "action": "export" # }, # { # "acl": 40, # "action": "import" # } # ], # "maximum_prefix": 34 # } # ], # "network": [ # { # "address": "192.1.13.0/24", # "backdoor": true # } # ], # "redistribute": [ # { # "protocol": "connected", # "route_map": "map01" # }, # { # "metric": 45, # "protocol": "kernel" # } # ], # "timers": { # "keepalive": 35 # } # }, # "before": {}, # "changed": true, # "commands": [ # "set protocols bgp neighbor 192.0.2.25 disable-connected-check", # "set protocols bgp neighbor 192.0.2.25 timers holdtime 30", # "set protocols bgp neighbor 192.0.2.25 timers keepalive 10", # "set protocols bgp neighbor 203.0.113.5 attribute-unchanged as-path", # "set protocols bgp neighbor 203.0.113.5 attribute-unchanged med", # "set protocols bgp neighbor 203.0.113.5 attribute-unchanged next-hop", # "set protocols bgp neighbor 203.0.113.5 ebgp-multihop 2", # "set protocols bgp neighbor 203.0.113.5 remote-as 101", # "set protocols bgp neighbor 203.0.113.5 update-source 192.0.2.25", # "set protocols bgp neighbor 5001::64 maximum-prefix 34", # "set protocols bgp neighbor 5001::64 distribute-list export 20", # "set protocols bgp neighbor 5001::64 distribute-list import 40", # "set protocols bgp redistribute kernel metric 45", # "set protocols bgp redistribute connected route-map map01", # "set protocols bgp network 192.1.13.0/24 backdoor", # "set protocols bgp aggregate-address 203.0.113.0/24 as-set", # "set protocols bgp aggregate-address 192.0.2.0/24 summary-only", # "set protocols bgp parameters bestpath as-path confed", # "set protocols bgp parameters bestpath compare-routerid", # "set protocols bgp parameters default no-ipv4-unicast", # "set protocols bgp parameters router-id 192.1.2.9", # "set protocols bgp parameters confederation peers 20", # "set protocols bgp parameters confederation peers 55", # "set protocols bgp parameters confederation identifier 66", # "set protocols bgp maximum-paths ebgp 20", # "set protocols bgp maximum-paths ibgp 55", # "set protocols bgp timers keepalive 35" # ], # Using replaced: # -------------- # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # set protocols bgp system-as 65536 # set protocols bgp aggregate-address 192.0.2.0/24 'summary-only' # set protocols bgp aggregate-address 203.0.113.0/24 'as-set' # set protocols bgp maximum-paths ebgp '20' # set protocols bgp maximum-paths ibgp '55' # set protocols bgp neighbor 192.0.2.25 'disable-connected-check' # set protocols bgp neighbor 192.0.2.25 timers holdtime '30' # set protocols bgp neighbor 192.0.2.25 timers keepalive '10' # set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'as-path' # set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'med' # set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'next-hop' # set protocols bgp neighbor 203.0.113.5 ebgp-multihop '2' # set protocols bgp neighbor 203.0.113.5 remote-as '101' # set protocols bgp neighbor 203.0.113.5 update-source '192.0.2.25' # set protocols bgp neighbor 5001::64 distribute-list export '20' # set protocols bgp neighbor 5001::64 distribute-list import '40' # set protocols bgp neighbor 5001::64 maximum-prefix '34' # set protocols bgp network 192.1.13.0/24 'backdoor' # set protocols bgp parameters bestpath as-path 'confed' # set protocols bgp parameters bestpath 'compare-routerid' # set protocols bgp parameters confederation identifier '66' # set protocols bgp parameters confederation peers '20' # set protocols bgp parameters confederation peers '55' # set protocols bgp parameters default 'no-ipv4-unicast' # set protocols bgp parameters router-id '192.1.2.9' # set protocols bgp redistribute connected route-map 'map01' # set protocols bgp redistribute kernel metric '45' # set protocols bgp timers keepalive '35' # vyos@vyos:~$ - name: Replace vyos.vyos.vyos_bgp_global: config: as_number: "65536" network: - address: "203.0.113.0/24" route_map: map01 redistribute: - protocol: "static" route_map: "map01" neighbor: - address: "192.0.2.40" advertisement_interval: 72 capability: orf: "receive" bgp_params: bestpath: as_path: "confed" state: replaced # After state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # set protocols bgp system-as 65536 # set protocols bgp neighbor 192.0.2.40 advertisement-interval '72' # set protocols bgp neighbor 192.0.2.40 capability orf prefix-list 'receive' # set protocols bgp network 203.0.113.0/24 route-map 'map01' # set protocols bgp parameters bestpath as-path 'confed' # set protocols bgp redistribute static route-map 'map01' # vyos@vyos:~$ # # # Module Execution: # # "after": { # "as_number": 65536, # "bgp_params": { # "bestpath": { # "as_path": "confed" # } # }, # "neighbor": [ # { # "address": "192.0.2.40", # "advertisement_interval": 72, # "capability": { # "orf": "receive" # } # } # ], # "network": [ # { # "address": "203.0.113.0/24", # "route_map": "map01" # } # ], # "redistribute": [ # { # "protocol": "static", # "route_map": "map01" # } # ] # }, # "before": { # "aggregate_address": [ # { # "prefix": "192.0.2.0/24", # "summary_only": true # }, # { # "prefix": "203.0.113.0/24", # "as_set": true # } # ], # "as_number": 65536, # "bgp_params": { # "bestpath": { # "as_path": "confed", # "compare_routerid": true # }, # "confederation": [ # { # "identifier": 66 # }, # { # "peers": 20 # }, # { # "peers": 55 # } # ], # "default": { # "no_ipv4_unicast": true # }, # "router_id": "192.1.2.9" # }, # "maximum_paths": [ # { # "count": 20, # "path": "ebgp" # }, # { # "count": 55, # "path": "ibgp" # } # ], # "neighbor": [ # { # "address": "192.0.2.25", # "disable_connected_check": true, # "timers": { # "holdtime": 30, # "keepalive": 10 # } # }, # { # "address": "203.0.113.5", # "attribute_unchanged": { # "as_path": true, # "med": true, # "next_hop": true # }, # "ebgp_multihop": 2, # "remote_as": 101, # "update_source": "192.0.2.25" # }, # { # "address": "5001::64", # "distribute_list": [ # { # "acl": 20, # "action": "export" # }, # { # "acl": 40, # "action": "import" # } # ], # "maximum_prefix": 34 # } # ], # "network": [ # { # "address": "192.1.13.0/24", # "backdoor": true # } # ], # "redistribute": [ # { # "protocol": "connected", # "route_map": "map01" # }, # { # "metric": 45, # "protocol": "kernel" # } # ], # "timers": { # "keepalive": 35 # } # }, # "changed": true, # "commands": [ # "delete protocols bgp timers", # "delete protocols bgp maximum-paths ", # "delete protocols bgp maximum-paths ", # "delete protocols bgp parameters router-id 192.1.2.9", # "delete protocols bgp parameters default", # "delete protocols bgp parameters confederation", # "delete protocols bgp parameters bestpath compare-routerid", # "delete protocols bgp aggregate-address", # "delete protocols bgp network 192.1.13.0/24", # "delete protocols bgp redistribute kernel", # "delete protocols bgp redistribute kernel", # "delete protocols bgp redistribute connected", # "delete protocols bgp redistribute connected", # "delete protocols bgp neighbor 5001::64", # "delete protocols bgp neighbor 203.0.113.5", # "delete protocols bgp neighbor 192.0.2.25", # "set protocols bgp neighbor 192.0.2.40 advertisement-interval 72", # "set protocols bgp neighbor 192.0.2.40 capability orf prefix-list receive", # "set protocols bgp redistribute static route-map map01", # "set protocols bgp network 203.0.113.0/24 route-map map01" # ], # Using deleted: # ------------- # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # set protocols bgp system-as 65536 # set protocols bgp neighbor 192.0.2.40 advertisement-interval '72' # set protocols bgp neighbor 192.0.2.40 capability orf prefix-list 'receive' # set protocols bgp network 203.0.113.0/24 route-map 'map01' # set protocols bgp parameters bestpath as-path 'confed' # set protocols bgp redistribute static route-map 'map01' # vyos@vyos:~$ - name: Delete configuration vyos.vyos.vyos_bgp_global: config: as_number: "65536" state: deleted # After state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # set protocols bgp '65536' # vyos@vyos:~$ # # # Module Execution: # # "after": { # "as_number": 65536 # }, # "before": { # "as_number": 65536, # "bgp_params": { # "bestpath": { # "as_path": "confed" # } # }, # "neighbor": [ # { # "address": "192.0.2.40", # "advertisement_interval": 72, # "capability": { # "orf": "receive" # } # } # ], # "network": [ # { # "address": "203.0.113.0/24", # "route_map": "map01" # } # ], # "redistribute": [ # { # "protocol": "static", # "route_map": "map01" # } # ] # }, # "changed": true, # "commands": [ # "delete protocols bgp neighbor 192.0.2.40", # "delete protocols bgp redistribute", # "delete protocols bgp network", # "delete protocols bgp parameters" # ], # Using purged: # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # set protocols bgp system-as 65536 # set protocols bgp aggregate-address 192.0.2.0/24 'summary-only' # set protocols bgp aggregate-address 203.0.113.0/24 'as-set' # set protocols bgp maximum-paths ebgp '20' # set protocols bgp maximum-paths ibgp '55' # set protocols bgp neighbor 192.0.2.25 'disable-connected-check' # set protocols bgp neighbor 192.0.2.25 timers holdtime '30' # set protocols bgp neighbor 192.0.2.25 timers keepalive '10' # set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'as-path' # set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'med' # set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'next-hop' # set protocols bgp neighbor 203.0.113.5 ebgp-multihop '2' # set protocols bgp neighbor 203.0.113.5 remote-as '101' # set protocols bgp neighbor 203.0.113.5 update-source '192.0.2.25' # set protocols bgp neighbor 5001::64 distribute-list export '20' # set protocols bgp neighbor 5001::64 distribute-list import '40' # set protocols bgp neighbor 5001::64 maximum-prefix '34' # set protocols bgp network 192.1.13.0/24 'backdoor' # set protocols bgp parameters bestpath as-path 'confed' # set protocols bgp parameters bestpath 'compare-routerid' # set protocols bgp parameters confederation identifier '66' # set protocols bgp parameters confederation peers '20' # set protocols bgp parameters confederation peers '55' # set protocols bgp parameters default 'no-ipv4-unicast' # set protocols bgp parameters router-id '192.1.2.9' # set protocols bgp redistribute connected route-map 'map01' # set protocols bgp redistribute kernel metric '45' # set protocols bgp timers keepalive '35' # vyos@vyos:~$ - name: Purge configuration vyos.vyos.vyos_bgp_global: config: as_number: "65536" state: purged # After state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # vyos@vyos:~$ # # Module Execution: # # "after": {}, # "before": { # "aggregate_address": [ # { # "prefix": "192.0.2.0/24", # "summary_only": true # }, # { # "prefix": "203.0.113.0/24", # "as_set": true # } # ], # "as_number": 65536, # "bgp_params": { # "bestpath": { # "as_path": "confed", # "compare_routerid": true # }, # "confederation": [ # { # "identifier": 66 # }, # { # "peers": 20 # }, # { # "peers": 55 # } # ], # "default": { # "no_ipv4_unicast": true # }, # "router_id": "192.1.2.9" # }, # "maximum_paths": [ # { # "count": 20, # "path": "ebgp" # }, # { # "count": 55, # "path": "ibgp" # } # ], # "neighbor": [ # { # "address": "192.0.2.25", # "disable_connected_check": true, # "timers": { # "holdtime": 30, # "keepalive": 10 # } # }, # { # "address": "203.0.113.5", # "attribute_unchanged": { # "as_path": true, # "med": true, # "next_hop": true # }, # "ebgp_multihop": 2, # "remote_as": 101, # "update_source": "192.0.2.25" # }, # { # "address": "5001::64", # "distribute_list": [ # { # "acl": 20, # "action": "export" # }, # { # "acl": 40, # "action": "import" # } # ], # "maximum_prefix": 34 # } # ], # "network": [ # { # "address": "192.1.13.0/24", # "backdoor": true # } # ], # "redistribute": [ # { # "protocol": "connected", # "route_map": "map01" # }, # { # "metric": 45, # "protocol": "kernel" # } # ], # "timers": { # "keepalive": 35 # } # }, # "changed": true, # "commands": [ # "delete protocols bgp 65536" # ], # Deleted in presence of address family under neighbors: # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # set protocols bgp system-as 65536 # set protocols bgp neighbor 192.0.2.43 advertisement-interval '72' # set protocols bgp neighbor 192.0.2.43 capability 'dynamic' # set protocols bgp neighbor 192.0.2.43 'disable-connected-check' # set protocols bgp neighbor 192.0.2.43 timers holdtime '30' # set protocols bgp neighbor 192.0.2.43 timers keepalive '10' # set protocols bgp neighbor 203.0.113.0 address-family 'ipv6-unicast' # set protocols bgp neighbor 203.0.113.0 capability orf prefix-list 'receive' # set protocols bgp network 203.0.113.0/24 route-map 'map01' # set protocols bgp parameters 'always-compare-med' # set protocols bgp parameters bestpath as-path 'confed' # set protocols bgp parameters bestpath 'compare-routerid' # set protocols bgp parameters dampening half-life '33' # set protocols bgp parameters dampening max-suppress-time '20' # set protocols bgp parameters dampening re-use '60' # set protocols bgp parameters dampening start-suppress-time '5' # set protocols bgp parameters default 'no-ipv4-unicast' # set protocols bgp parameters distance global external '66' # set protocols bgp parameters distance global internal '20' # set protocols bgp parameters distance global local '10' # set protocols bgp redistribute static route-map 'map01' # vyos@vyos:~$ ^C # vyos@vyos:~$ - name: Delete configuration vyos.vyos.vyos_bgp_global: config: as_number: "65536" state: deleted # Module Execution: # # "changed": false, # "invocation": { # "module_args": { # "config": { # "aggregate_address": null, # "as_number": 65536, # "bgp_params": null, # "maximum_paths": null, # "neighbor": null, # "network": null, # "redistribute": null, # "timers": null # }, # "running_config": null, # "state": "deleted" # } # }, # "msg": "Use the _bgp_address_family module to delete the address_family under neighbor 203.0.113.0, before replacing/deleting the neighbor." # } # using gathered: # -------------- # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" # set protocols bgp system-as 65536 # set protocols bgp neighbor 192.0.2.43 advertisement-interval '72' # set protocols bgp neighbor 192.0.2.43 capability 'dynamic' # set protocols bgp neighbor 192.0.2.43 'disable-connected-check' # set protocols bgp neighbor 192.0.2.43 timers holdtime '30' # set protocols bgp neighbor 192.0.2.43 timers keepalive '10' # set protocols bgp neighbor 203.0.113.0 address-family 'ipv6-unicast' # set protocols bgp neighbor 203.0.113.0 capability orf prefix-list 'receive' # set protocols bgp network 203.0.113.0/24 route-map 'map01' # set protocols bgp parameters 'always-compare-med' # set protocols bgp parameters bestpath as-path 'confed' # set protocols bgp parameters bestpath 'compare-routerid' # set protocols bgp parameters dampening half-life '33' # set protocols bgp parameters dampening max-suppress-time '20' # set protocols bgp parameters dampening re-use '60' # set protocols bgp parameters dampening start-suppress-time '5' # set protocols bgp parameters default 'no-ipv4-unicast' # set protocols bgp parameters distance global external '66' # set protocols bgp parameters distance global internal '20' # set protocols bgp parameters distance global local '10' # set protocols bgp redistribute static route-map 'map01' # vyos@vyos:~$ ^C - name: gather configs vyos.vyos.vyos_bgp_global: state: gathered # Module Execution: # "gathered": { # "as_number": 65536, # "bgp_params": { # "always_compare_med": true, # "bestpath": { # "as_path": "confed", # "compare_routerid": true # }, # "default": { # "no_ipv4_unicast": true # }, # "distance": [ # { # "type": "external", # "value": 66 # }, # { # "type": "internal", # "value": 20 # }, # { # "type": "local", # "value": 10 # } # ] # }, # "neighbor": [ # { # "address": "192.0.2.43", # "advertisement_interval": 72, # "capability": { # "dynamic": true # }, # "disable_connected_check": true, # "timers": { # "holdtime": 30, # "keepalive": 10 # } # }, # { # "address": "203.0.113.0", # "capability": { # "orf": "receive" # } # } # ], # "network": [ # { # "address": "203.0.113.0/24", # "route_map": "map01" # } # ], # "redistribute": [ # { # "protocol": "static", # "route_map": "map01" # } # ] # }, # # Using parsed: # ------------ # parsed.cfg # set protocols bgp neighbor 192.0.2.43 advertisement-interval '72' # set protocols bgp neighbor 192.0.2.43 capability 'dynamic' # set protocols bgp neighbor 192.0.2.43 'disable-connected-check' # set protocols bgp neighbor 192.0.2.43 timers holdtime '30' # set protocols bgp neighbor 192.0.2.43 timers keepalive '10' # set protocols bgp neighbor 203.0.113.0 address-family 'ipv6-unicast' # set protocols bgp neighbor 203.0.113.0 capability orf prefix-list 'receive' # set protocols bgp network 203.0.113.0/24 route-map 'map01' # set protocols bgp parameters 'always-compare-med' # set protocols bgp parameters bestpath as-path 'confed' # set protocols bgp parameters bestpath 'compare-routerid' # set protocols bgp parameters dampening half-life '33' # set protocols bgp parameters dampening max-suppress-time '20' # set protocols bgp parameters dampening re-use '60' # set protocols bgp parameters dampening start-suppress-time '5' # set protocols bgp parameters default 'no-ipv4-unicast' # set protocols bgp parameters distance global external '66' # set protocols bgp parameters distance global internal '20' # set protocols bgp parameters distance global local '10' # set protocols bgp redistribute static route-map 'map01' - name: parse configs vyos.vyos.vyos_bgp_global: running_config: "{{ lookup('file', './parsed.cfg') }}" state: parsed tags: - parsed # Module execution: # "parsed": { # "as_number": 65536, # "bgp_params": { # "always_compare_med": true, # "bestpath": { # "as_path": "confed", # "compare_routerid": true # }, # "default": { # "no_ipv4_unicast": true # }, # "distance": [ # { # "type": "external", # "value": 66 # }, # { # "type": "internal", # "value": 20 # }, # { # "type": "local", # "value": 10 # } # ] # }, # "neighbor": [ # { # "address": "192.0.2.43", # "advertisement_interval": 72, # "capability": { # "dynamic": true # }, # "disable_connected_check": true, # "timers": { # "holdtime": 30, # "keepalive": 10 # } # }, # { # "address": "203.0.113.0", # "capability": { # "orf": "receive" # } # } # ], # "network": [ # { # "address": "203.0.113.0/24", # "route_map": "map01" # } # ], # "redistribute": [ # { # "protocol": "static", # "route_map": "map01" # } # ] # } # # Using rendered: # -------------- - name: Render vyos.vyos.vyos_bgp_global: config: as_number: "65536" network: - address: "203.0.113.0/24" route_map: map01 redistribute: - protocol: "static" route_map: "map01" bgp_params: always_compare_med: true dampening: start_suppress_time: 5 max_suppress_time: 20 half_life: 33 re_use: 60 distance: - type: "internal" value: 20 - type: "local" value: 10 - type: "external" value: 66 bestpath: as_path: "confed" compare_routerid: true default: no_ipv4_unicast: true neighbor: - address: "192.0.2.43" disable_connected_check: true advertisement_interval: 72 capability: dynamic: true timers: holdtime: 30 keepalive: 10 - address: "203.0.113.0" capability: orf: "receive" state: rendered # Module Execution: # "rendered": [ # "set protocols bgp neighbor 192.0.2.43 disable-connected-check", # "set protocols bgp neighbor 192.0.2.43 advertisement-interval 72", # "set protocols bgp neighbor 192.0.2.43 capability dynamic", # "set protocols bgp neighbor 192.0.2.43 timers holdtime 30", # "set protocols bgp neighbor 192.0.2.43 timers keepalive 10", # "set protocols bgp neighbor 203.0.113.0 capability orf prefix-list receive", # "set protocols bgp redistribute static route-map map01", # "set protocols bgp network 203.0.113.0/24 route-map map01", # "set protocols bgp parameters always-compare-med", # "set protocols bgp parameters dampening half-life 33", # "set protocols bgp parameters dampening max-suppress-time 20", # "set protocols bgp parameters dampening re-use 60", # "set protocols bgp parameters dampening start-suppress-time 5", # "set protocols bgp parameters distance global internal 20", # "set protocols bgp parameters distance global local 10", # "set protocols bgp parameters distance global external 66", # "set protocols bgp parameters bestpath as-path confed", # "set protocols bgp parameters bestpath compare-routerid", # "set protocols bgp parameters default no-ipv4-unicast" # ] """ RETURN = """ before: description: The configuration prior to the module execution. returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) type: dict sample: > This output will always be in the same format as the module argspec. after: description: The resulting configuration after module execution. returned: when changed type: dict sample: > This output will always be in the same format as the module argspec. commands: description: The set of commands pushed to the remote device. returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) type: list sample: - set protocols bgp redistribute static route-map map01 - set protocols bgp network 203.0.113.0/24 route-map map01 - set protocols bgp parameters always-compare-med rendered: description: The provided configuration in the task rendered in device-native format (offline). returned: when I(state) is C(rendered) type: list sample: - set protocols bgp redistribute static route-map map01 - set protocols bgp network 203.0.113.0/24 route-map map01 - set protocols bgp parameters always-compare-med gathered: description: Facts about the network resource gathered from the remote device as structured data. returned: when I(state) is C(gathered) type: list sample: > This output will always be in the same format as the module argspec. parsed: description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. returned: when I(state) is C(parsed) type: list sample: > This output will always be in the same format as the module argspec. """ from ansible.module_utils.basic import AnsibleModule from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.bgp_global.bgp_global import ( Bgp_globalArgs, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_global.bgp_global import ( Bgp_global, ) def main(): """ Main entry point for module execution :returns: the result form module invocation """ module = AnsibleModule( argument_spec=Bgp_globalArgs.argument_spec, mutually_exclusive=[["config", "running_config"]], required_if=[ ["state", "merged", ["config"]], ["state", "replaced", ["config"]], ["state", "overridden", ["config"]], ["state", "rendered", ["config"]], ["state", "parsed", ["running_config"]], ], supports_check_mode=True, ) result = Bgp_global(module).execute_module() module.exit_json(**result) if __name__ == "__main__": main() 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 @@ -1,419 +1,446 @@ #!/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 - 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: 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", "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 }}" 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 @@ -1,119 +1,158 @@ # (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 . +# Make coding more python3-ish 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 -""" +from .vyos_module import TestVyosModule, load_fixture 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] + 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")