diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 38eae56f..b23fb30b 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,591 +1,591 @@ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json # CodeRabbit configuration for vyos/vyos.vyos Ansible network collection # Docs: https://docs.coderabbit.ai/guides/configure-coderabbit language: en-US early_access: false tone_instructions: > Concise, technical, no filler. Focus on correctness, security, idempotency, and Ansible conventions. Cite file paths and line numbers. reviews: profile: chill request_changes_workflow: false high_level_summary: true - high_level_summary_placeholder: '@coderabbitai summary' - auto_title_placeholder: '@coderabbitai' + high_level_summary_placeholder: "@coderabbitai summary" + auto_title_placeholder: "@coderabbitai" review_status: true poem: false collapse_walkthrough: true changed_files_summary: true sequence_diagrams: false assess_linked_issues: true related_issues: true related_prs: true suggested_labels: false auto_apply_labels: false suggested_reviewers: false auto_review: enabled: true auto_incremental_review: true drafts: false base_branches: - main ignore_title_keywords: - WIP - DO NOT MERGE - Bump path_filters: - - '!**/__pycache__/**' - - '!**/*.pyc' - - '!**/*.egg-info/**' - - '!changelogs/changelog.yaml' - - '!.venv/**' - - '!.collections/**' - - '!.worktrees/**' + - "!**/__pycache__/**" + - "!**/*.pyc" + - "!**/*.egg-info/**" + - "!changelogs/changelog.yaml" + - "!.venv/**" + - "!.collections/**" + - "!.worktrees/**" path_instructions: # ── Global PR hygiene ────────────────────────────────────────────── - - path: '**' + - 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' + - 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/**' + - 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/**' + - 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/**' + - 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' + - 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' + - 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' + - 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' + - 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}' + - 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/**' + - 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/**' + - 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/**' + - 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' + - 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' + - 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' + - 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/**' + - 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/**' + - 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/**' + - 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' + - 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' + - 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' + - 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' + - path: "plugins/action/vyos.py" instructions: | Skip — thin proxy inheriting ActionNetworkModule. No docstrings needed. # ── Utilities ──────────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/utils/*.py' + - 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/**' + - 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' + - 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' + - path: "plugins/modules/vyos_l3_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_lag_interfaces.py' + - path: "plugins/modules/vyos_lag_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_lldp_global.py' + - path: "plugins/modules/vyos_lldp_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_lldp_interfaces.py' + - path: "plugins/modules/vyos_lldp_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_static_routes.py' + - path: "plugins/modules/vyos_static_routes.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_firewall_rules.py' + - path: "plugins/modules/vyos_firewall_rules.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_firewall_global.py' + - path: "plugins/modules/vyos_firewall_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_firewall_interfaces.py' + - path: "plugins/modules/vyos_firewall_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_ospfv2.py' + - path: "plugins/modules/vyos_ospfv2.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_ospfv3.py' + - path: "plugins/modules/vyos_ospfv3.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_ospf_interfaces.py' + - path: "plugins/modules/vyos_ospf_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_bgp_global.py' + - path: "plugins/modules/vyos_bgp_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_bgp_address_family.py' + - path: "plugins/modules/vyos_bgp_address_family.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_prefix_lists.py' + - path: "plugins/modules/vyos_prefix_lists.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_route_maps.py' + - path: "plugins/modules/vyos_route_maps.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_snmp_server.py' + - path: "plugins/modules/vyos_snmp_server.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_logging_global.py' + - path: "plugins/modules/vyos_logging_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_ntp_global.py' + - path: "plugins/modules/vyos_ntp_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_hostname.py' + - path: "plugins/modules/vyos_hostname.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_vrf.py' + - path: "plugins/modules/vyos_vrf.py" instructions: *resource_module_tests # ── Legacy module tests ────────────────────────────────────────── - - path: 'plugins/modules/vyos_command.py' + - 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' + - path: "plugins/modules/vyos_config.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_facts.py' + - path: "plugins/modules/vyos_facts.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_banner.py' + - path: "plugins/modules/vyos_banner.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_ping.py' + - path: "plugins/modules/vyos_ping.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_system.py' + - path: "plugins/modules/vyos_system.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_user.py' + - path: "plugins/modules/vyos_user.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_vlan.py' + - path: "plugins/modules/vyos_vlan.py" instructions: *legacy_module_tests # ── Test infrastructure — do not generate tests for these ──────── - - path: 'tests/unit/modules/utils.py' + - 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' + - 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' + - 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/**' + - 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/**' + - 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' + - 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' + - 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' + - path: "plugins/action/vyos.py" instructions: | Skip — thin action proxy. Tested indirectly via module tests. diff --git a/docs/vyos.vyos.vyos_nat_module.rst b/docs/vyos.vyos.vyos_nat_module.rst index 0db03960..87f2284b 100644 --- a/docs/vyos.vyos.vyos_nat_module.rst +++ b/docs/vyos.vyos.vyos_nat_module.rst @@ -1,3293 +1,3337 @@ .. _vyos.vyos.vyos_nat_module: ****************** vyos.vyos.vyos_nat ****************** **NAT resource module** Version added: 1.0.0 .. contents:: :local: :depth: 1 Synopsis -------- - This module manages NAT configuration on devices running VyOS. Parameters ---------- .. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
Parameter Choices/Defaults Comments
config
dictionary
The desired configuration for the NAT resource represented as a dictionary.
nat
dictionary
Configuration for NAT rules.
cgnat
dictionary
Configuration for Carrier Grade NAT (CGNAT).
log_allocation
boolean
    Choices:
  • no
  • yes
Log CGNAT address allocations.
pool
dictionary
Configuration for CGNAT pools.
external
list / elements=dictionary
List of external NAT pools for CGNAT.
external_port_range
string
Port range to use for NAT translations in this external pool.
name
string / required
Name of the external NAT pool.
per_user_limit
dictionary
Per-user limit configuration for the external pool.
port
string
Maximum number of ports allocated per user.
range
list - / elements=string + / elements=dictionary +
+
+ +
List of external IP address ranges in the pool.
+
+
+ seq + +
+ string
-
List of external IP addresses or prefixes in the pool.
+
Optional sequence number for this range entry.
+
+ value + +
+ string + / required +
+
+ +
IP address, prefix, or range (e.g. 203.0.113.0/24 or 203.0.113.1-203.0.113.60).
+
internal
list / elements=dictionary
List of internal NAT pools for CGNAT.
name
string / required
Name of the internal NAT pool.
range
list / elements=string
List of internal IP addresses or prefixes in the pool.
rule
list / elements=dictionary
List of CGNAT rules.
id
integer / required
Rule number for CGNAT.
source
dictionary
Source pool configuration for CGNAT translation.
pool
string
Source pool name to use for CGNAT translation.
translation
dictionary
Translation pool configuration for CGNAT.
pool
string
Translation pool name to use for CGNAT translation.
destination
dictionary
Configuration for destination NAT rules.
rule
list / elements=dictionary
List of destination NAT rules.
description
string
User-friendly description of the destination NAT rule.
destination
dictionary
Match criteria for destination NAT.
address
string
IP address, subnet, or range to match.
address_group
string
Address group name to match.
domain_group
string
Domain group name to match.
fqdn
string
Fully qualified domain name to match.
mac_group
string
MAC address group name to match.
network_group
string
Network group name to match.
port
string
Port number or range to match.
port_group
string
Port group name to match.
disable
boolean
    Choices:
  • no
  • yes
Disable this destination NAT rule.
exclude
boolean
    Choices:
  • no
  • yes
Exclude packets matching this rule from NAT.
id
integer / required
Rule number for destination NAT.
inbound_interface
dictionary
Match inbound interface.
group
string
Interface group to match.
name
string
Interface name to match.
log
boolean
    Choices:
  • no
  • yes
Log packets hitting this rule.
packet_type
string
Packet type to match.
protocol
string
Protocol to NAT (default all).
translation
dictionary
Translation configuration for destination NAT.
address
string
IP address or prefix to translate destination to.
address_mapping
string
    Choices:
  • random
  • persistent
Address mapping mode for translation.
port
string
Port number or range to translate destination port to.
port_mapping
string
    Choices:
  • random
  • none
Port mapping mode for translation.
redirect_port
string
Redirect to local port number.
source
dictionary
Configuration for source NAT rules.
rule
list / elements=dictionary
List of source NAT rules.
description
string
User-friendly description of the source NAT rule.
destination
dictionary
Destination match criteria for source NAT.
address
string
IP address, subnet, or range to match.
address_group
string
Address group name to match.
domain_group
string
Domain group name to match.
fqdn
string
Fully qualified domain name to match.
mac_group
string
MAC address group name to match.
network_group
string
Network group name to match.
port
string
Port number or range to match.
port_group
string
Port group name to match.
disable
boolean
    Choices:
  • no
  • yes
Disable this source NAT rule.
exclude
boolean
    Choices:
  • no
  • yes
Exclude packets matching this rule from NAT.
id
integer / required
Rule number for source NAT.
log
boolean
    Choices:
  • no
  • yes
Log packets hitting this rule.
outbound_interface
dictionary
Match outbound interface.
group
string
Interface group to match.
name
string
Interface name to match.
packet_type
string
Packet type to match.
protocol
string
Protocol to NAT (default all).
source
dictionary
Source match criteria for source NAT.
address
string
IP address, subnet, or range to match.
address_group
string
Address group name to match.
domain_group
string
Domain group name to match.
fqdn
string
Fully qualified domain name to match.
mac_group
string
MAC address group name to match.
network_group
string
Network group name to match.
port
string
Port number or range to match.
port_group
string
Port group name to match.
translation
dictionary
Translation configuration for source NAT.
address
string
IP address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address.
address_mapping
string
    Choices:
  • random
  • persistent
Address mapping mode for translation.
port
string
Port number or range to translate source port to.
port_mapping
string
    Choices:
  • random
  • none
Port mapping mode for translation.
static
dictionary
Configuration for static one-to-one NAT rules.
rule
list / elements=dictionary
List of static NAT rules.
description
string
User-friendly description of the static NAT rule.
destination
dictionary
Match criteria for static NAT.
address
string
IP address, subnet, or range to match.
id
integer / required
Rule number for static NAT.
inbound_interface
string
Inbound interface that this static NAT rule applies to.
log
boolean
    Choices:
  • no
  • yes
Log packets hitting this static NAT rule.
translation
dictionary
Translation configuration for static NAT.
address
string
IP address or prefix to translate to.
nat64
dictionary
Configuration for NAT64 (IPv6-to-IPv4) rules.
source
dictionary
Configuration for NAT64 source rules.
rule
list / elements=dictionary
List of NAT64 source rules.
description
string
User-friendly description of the NAT64 source rule.
disable
boolean
    Choices:
  • no
  • yes
Disable this NAT64 source rule.
id
integer / required
Rule number for NAT64 source rule (1-999999).
match
dictionary
Match criteria for NAT64 source rule.
mark
integer
Match on firewall mark value (1-2147483647).
source
dictionary
IPv6 source prefix to match for NAT64 translation.
prefix
string
IPv6 source prefix to match (h:h:h:h:h:h:h:h/x).
translation
dictionary
Translation configuration for NAT64 source rule.
pool
list / elements=dictionary
List of translation pools for NAT64.
address
string
IPv4 address or prefix for translation pool.
description
string
User-friendly description of the translation pool.
disable
boolean
    Choices:
  • no
  • yes
Disable this translation pool.
id
integer / required
Pool number (1-999999).
port
string
Port number or range for translation pool.
protocol
string
    Choices:
  • icmp
  • tcp
  • udp
Protocol for this translation pool entry.
nat66
dictionary
Configuration for NAT66 (IPv6-to-IPv6) rules.
destination
dictionary
Configuration for NAT66 destination rules.
rule
list / elements=dictionary
List of NAT66 destination rules.
description
string
User-friendly description of the NAT66 destination rule.
destination
dictionary
Match criteria for NAT66 destination rule.
address
string
IPv6 address or prefix to match.
port
string
Port number or range to match.
disable
boolean
    Choices:
  • no
  • yes
Disable this NAT66 destination rule.
exclude
boolean
    Choices:
  • no
  • yes
Exclude packets matching this rule from NAT66.
id
integer / required
Rule number for NAT66 destination rule.
inbound_interface
dictionary
Inbound interface to match for NAT66 destination rule.
name
string
Interface name to match.
log
boolean
    Choices:
  • no
  • yes
Log packets hitting this NAT66 destination rule.
protocol
string
Protocol to match.
source
dictionary
Source match criteria for NAT66 destination rule.
address
string
IPv6 source address or prefix to match.
port
string
Source port number or range to match.
translation
dictionary
Translation configuration for NAT66 destination rule.
address
string
IPv6 address or prefix to translate destination to.
port
string
Port number or range to translate destination port to.
source
dictionary
Configuration for NAT66 source rules.
rule
list / elements=dictionary
List of NAT66 source rules.
description
string
User-friendly description of the NAT66 source rule.
destination
dictionary
Destination match criteria for NAT66 source rule.
port
string
Destination port number or range to match.
prefix
string
IPv6 destination prefix to match (h:h:h:h:h:h:h:h/x).
disable
boolean
    Choices:
  • no
  • yes
Disable this NAT66 source rule.
exclude
boolean
    Choices:
  • no
  • yes
Exclude packets matching this rule from NAT66.
id
integer / required
Rule number for NAT66 source rule.
log
boolean
    Choices:
  • no
  • yes
Log packets hitting this NAT66 source rule.
outbound_interface
dictionary
Outbound interface to match for NAT66 source rule.
name
string
Interface name to match.
protocol
string
Protocol to match.
source
dictionary
Source match criteria for NAT66 source rule.
port
string
Source port number or range to match.
prefix
string
IPv6 source prefix to match (h:h:h:h:h:h:h:h/x).
translation
dictionary
Translation configuration for NAT66 source rule.
address
string
IPv6 address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address.
port
string
Port number or range to translate source port to.
running_config
string
This option is used only with state parsed.
The value of this option should be the output received from the VyOS device by executing the command show configuration commands | grep nat.
The state parsed reads the configuration from show configuration commands | grep nat and transforms it into Ansible structured data as per the module argspec. The value is then returned in the parsed key within the result.
The states replaced and overridden have identical behaviour for this module.
state
string
    Choices:
  • deleted
  • merged ←
  • overridden
  • replaced
  • gathered
  • rendered
  • parsed
The state the configuration should be left in.

Notes ----- .. note:: - 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 ``network_cli``. Examples -------- .. code-block:: yaml # Using merged - configure CGNAT - name: Merge CGNAT configuration vyos.vyos.vyos_nat: config: nat: cgnat: log_allocation: true pool: external: - name: ext-pool-1 external_port_range: "10000-20000" per_user_limit: port: "200" range: - 203.0.113.0/24 internal: - name: int-pool-1 range: - 10.0.0.0/24 rule: - id: 1 source: pool: int-pool-1 translation: pool: ext-pool-1 state: merged # Using merged - configure destination NAT - name: Merge destination NAT rule vyos.vyos.vyos_nat: config: nat: destination: rule: - id: 100 description: "Web server NAT" protocol: tcp log: true destination: address: 198.51.100.10 port: "80" translation: address: 192.168.1.10 port: "8080" state: merged # Using merged - configure source NAT - name: Merge source NAT rule vyos.vyos.vyos_nat: config: nat: source: rule: - id: 200 description: "Outbound masquerade" protocol: tcp log: true outbound_interface: name: eth0 translation: address: masquerade state: merged # Using merged - configure static NAT - name: Merge static NAT rule vyos.vyos.vyos_nat: config: nat: static: rule: - id: 300 description: "Static mapping" inbound_interface: eth2 destination: address: 198.51.100.20 translation: address: 192.168.1.20 log: true state: merged # Using merged - configure NAT64 - name: Merge NAT64 source rule vyos.vyos.vyos_nat: config: nat64: source: rule: - id: 10 description: "NAT64 example" source: prefix: 2001:db8::/96 match: mark: "100" translation: pool: - id: 1 address: 192.168.100.10 port: "1-65535" protocol: udp state: merged # Using merged - configure NAT66 - name: Merge NAT66 destination rule vyos.vyos.vyos_nat: config: nat66: destination: rule: - id: 20 description: "NAT66 DNAT" protocol: tcp inbound_interface: name: eth1 destination: address: 2001:db8::1 translation: address: 2001:db8:1::10 port: "8443" state: merged # Using gathered - name: Gather NAT config vyos.vyos.vyos_nat: state: gathered # Using deleted - name: Delete all NAT config vyos.vyos.vyos_nat: state: deleted # Using replaced - name: Replace NAT source rules vyos.vyos.vyos_nat: config: nat: source: rule: - id: 200 description: "Replaced outbound rule" translation: address: masquerade state: replaced # Using parsed - name: Parse NAT config from file vyos.vyos.vyos_nat: running_config: "{{ lookup('file', './nat_config.cfg') }}" state: parsed # Using rendered - name: Render NAT config offline vyos.vyos.vyos_nat: config: nat: source: rule: - id: 200 description: "Rendered rule" translation: address: masquerade state: rendered Status ------ Authors ~~~~~~~ - Evgeny Molotkov (@omnom62) diff --git a/plugins/module_utils/network/vyos/argspec/nat/nat.py b/plugins/module_utils/network/vyos/argspec/nat/nat.py index 0cce17af..17b5d4c8 100644 --- a/plugins/module_utils/network/vyos/argspec/nat/nat.py +++ b/plugins/module_utils/network/vyos/argspec/nat/nat.py @@ -1,596 +1,600 @@ # -*- 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) from __future__ import absolute_import, division, print_function __metaclass__ = type """ The arg spec for the vyos_nat module """ class NatArgs(object): # pylint: disable=R0903 """The arg spec for the vyos_nat module""" argument_spec = { "config": { "type": "dict", "nat": { "type": "dict", "options": { "cgnat": { "type": "dict", "options": { "log_allocation": { "type": "bool", }, "pool": { "type": "dict", "options": { "external": { "type": "list", "elements": "dict", "options": { "name": { "type": "str", "required": True, }, "external_port_range": { "type": "str", }, "per_user_limit": { "type": "dict", "options": { "port": { "type": "str", }, }, }, "range": { "type": "list", - "elements": "str", + "elements": "dict", + "options": { + "value": {"type": "str", "required": True}, + "seq": {"type": "str"}, + }, }, }, }, "internal": { "type": "list", "elements": "dict", "options": { "name": { "type": "str", "required": True, }, "range": { "type": "list", "elements": "str", }, }, }, }, }, "rule": { "type": "list", "elements": "dict", "options": { "id": { "type": "int", "required": True, }, "source": { "type": "dict", "options": { "pool": { "type": "str", }, }, }, "translation": { "type": "dict", "options": { "pool": { "type": "str", }, }, }, }, }, }, }, "destination": { "type": "dict", "options": { "rule": { "type": "list", "elements": "dict", "options": { "id": { "type": "int", "required": True, }, "description": { "type": "str", }, "protocol": { "type": "str", }, "packet_type": { "type": "str", }, "exclude": { "type": "bool", }, "log": { "type": "bool", }, "disable": { "type": "bool", }, "inbound_interface": { "type": "dict", "options": { "name": { "type": "str", }, "group": { "type": "str", }, }, }, "destination": { "type": "dict", "options": { "address": { "type": "str", }, "fqdn": { "type": "str", }, "port": { "type": "str", }, "address_group": { "type": "str", }, "domain_group": { "type": "str", }, "mac_group": { "type": "str", }, "network_group": { "type": "str", }, "port_group": { "type": "str", }, }, }, "translation": { "type": "dict", "options": { "address": { "type": "str", }, "port": { "type": "str", }, "redirect_port": { "type": "str", }, "address_mapping": { "type": "str", "choices": [ "random", "persistent", ], }, "port_mapping": { "type": "str", "choices": [ "random", "none", ], }, }, }, }, }, }, }, "source": { "type": "dict", "options": { "rule": { "type": "list", "elements": "dict", "options": { "id": { "type": "int", "required": True, }, "description": { "type": "str", }, "protocol": { "type": "str", }, "packet_type": { "type": "str", }, "exclude": { "type": "bool", }, "log": { "type": "bool", }, "disable": { "type": "bool", }, "outbound_interface": { "type": "dict", "options": { "name": { "type": "str", }, "group": { "type": "str", }, }, }, "destination": { "type": "dict", "options": { "address": { "type": "str", }, "fqdn": { "type": "str", }, "address_group": { "type": "str", }, "domain_group": { "type": "str", }, "mac_group": { "type": "str", }, "network_group": { "type": "str", }, "port_group": { "type": "str", }, "port": { "type": "str", }, }, }, "source": { "type": "dict", "options": { "address": {"type": "str"}, "fqdn": {"type": "str"}, "port": {"type": "str"}, "address_group": {"type": "str"}, "domain_group": {"type": "str"}, "mac_group": {"type": "str"}, "network_group": {"type": "str"}, "port_group": {"type": "str"}, }, }, "translation": { "type": "dict", "options": { "address": { "type": "str", }, "port": { "type": "str", }, "address_mapping": { "type": "str", "choices": [ "random", "persistent", ], }, "port_mapping": { "type": "str", "choices": [ "random", "none", ], }, }, }, }, }, }, }, "static": { "type": "dict", "options": { "rule": { "type": "list", "elements": "dict", "options": { "id": { "type": "int", "required": True, }, "description": { "type": "str", }, "destination": { "type": "dict", "options": { "address": { "type": "str", }, }, }, "inbound_interface": { "type": "str", }, "log": { "type": "bool", }, "translation": { "type": "dict", "options": { "address": { "type": "str", }, }, }, }, }, }, }, }, }, "nat64": { "type": "dict", "options": { "source": { "type": "dict", "options": { "rule": { "type": "list", "elements": "dict", "options": { "id": { "type": "int", "required": True, }, "description": { "type": "str", }, "disable": { "type": "bool", }, "match": { "type": "dict", "options": { "mark": { "type": "int", }, }, }, "source": { "type": "dict", "options": { "prefix": { "type": "str", }, }, }, "translation": { "type": "dict", "options": { "pool": { "type": "list", "elements": "dict", "options": { "id": { "type": "int", "required": True, }, "address": { "type": "str", }, "description": { "type": "str", }, "disable": { "type": "bool", }, "port": { "type": "str", }, "protocol": { "type": "str", "choices": [ "icmp", "tcp", "udp", ], }, }, }, }, }, }, }, }, }, }, }, "nat66": { "type": "dict", "options": { "destination": { "type": "dict", "options": { "rule": { "type": "list", "elements": "dict", "options": { "id": { "type": "int", "required": True, }, "description": { "type": "str", }, "destination": { "type": "dict", "options": { "address": { "type": "str", }, "port": { "type": "str", }, }, }, "disable": { "type": "bool", }, "exclude": { "type": "bool", }, "inbound_interface": { "type": "dict", "options": { "name": { "type": "str", }, }, }, "log": { "type": "bool", }, "protocol": { "type": "str", }, "source": { "type": "dict", "options": { "address": { "type": "str", }, "port": { "type": "str", }, }, }, "translation": { "type": "dict", "options": { "address": { "type": "str", }, "port": { "type": "str", }, }, }, }, }, }, }, "source": { "type": "dict", "options": { "rule": { "type": "list", "elements": "dict", "options": { "id": { "type": "int", "required": True, }, "description": { "type": "str", }, "destination": { "type": "dict", "options": { "port": { "type": "str", }, "prefix": { "type": "str", }, }, }, "disable": { "type": "bool", }, "exclude": { "type": "bool", }, "log": { "type": "bool", }, "outbound_interface": { "type": "dict", "options": { "name": { "type": "str", }, }, }, "protocol": { "type": "str", }, "source": { "type": "dict", "options": { "port": { "type": "str", }, "prefix": { "type": "str", }, }, }, "translation": { "type": "dict", "options": { "address": { "type": "str", }, "port": { "type": "str", }, }, }, }, }, }, }, }, }, }, "running_config": {"type": "str"}, "state": { "type": "str", "choices": [ "deleted", "merged", "overridden", "replaced", "gathered", "rendered", "parsed", ], "default": "merged", }, } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/facts/nat/nat.py b/plugins/module_utils/network/vyos/facts/nat/nat.py index 41e6ba31..a022252a 100644 --- a/plugins/module_utils/network/vyos/facts/nat/nat.py +++ b/plugins/module_utils/network/vyos/facts/nat/nat.py @@ -1,155 +1,174 @@ # -*- 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 ntp fact class It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ import re from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.nat.nat import ( NatArgs, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.nat import ( NatTemplate, ) class NatFacts(object): """The vyos nat facts class""" def __init__(self, module, subspec="config", options="options"): self._module = module self.argument_spec = NatArgs.argument_spec def get_config(self, connection): return connection.get("show configuration commands | match 'nat'") def populate_facts(self, connection, ansible_facts, data=None): """Populate the facts for NAT network resource :param connection: the device connection :param ansible_facts: Facts dictionary :param data: previously collected conf :rtype: dictionary :returns: facts """ facts = {} objs = [] config_lines = [] if not data: data = self.get_config(connection) for resource in data.splitlines(): config_lines.append(re.sub(r"'([^']*)'", r"\1", resource)) nat_parser = NatTemplate(lines=config_lines, module=self._module) objs = nat_parser.parse() objs = self._normalise(objs) ansible_facts["ansible_network_resources"].pop("nat", None) params = utils.remove_empties( nat_parser.validate_config(self.argument_spec, {"config": objs}, redact=True), ) if params.get("config"): facts["nat"] = params["config"] ansible_facts["ansible_network_resources"].update(facts) return ansible_facts def _deep_merge(self, base, override): for k, v in override.items(): if k in base and isinstance(base[k], dict) and isinstance(v, dict): self._deep_merge(base[k], v) elif k in base and isinstance(base[k], list) and isinstance(v, list): for entry in v: if entry not in base[k]: base[k].append(entry) else: base[k] = v return base def _merge_rule_list(self, rules): merged = {} for item in rules: rid = item["id"] if rid not in merged: merged[rid] = {"id": rid} for k, v in item.items(): if k == "id": continue if isinstance(v, list): existing = merged[rid].setdefault(k, []) for entry in v: if entry not in existing: existing.append(entry) elif isinstance(v, dict): merged[rid].setdefault(k, {}) self._deep_merge(merged[rid][k], v) else: merged[rid][k] = v return list(merged.values()) def _merge_pool_list(self, pools): merged = {} for item in pools: name = item["name"] if name not in merged: merged[name] = {"name": name} for k, v in item.items(): if k == "name": continue - if isinstance(v, list): + if k == "range" and isinstance(v, list): + existing = merged[name].setdefault(k, []) + existing.extend(v) + merged[name][k] = self._merge_range_list(existing) + elif isinstance(v, list): # ← elif not if merged[name].setdefault(k, []) for val in v: if val not in merged[name][k]: merged[name][k].append(val) elif isinstance(v, dict): merged[name].setdefault(k, {}) self._deep_merge(merged[name][k], v) else: merged[name][k] = v return list(merged.values()) def _normalise(self, objs): for nat_type in ["nat", "nat64", "nat66"]: nat = objs.get(nat_type) if not nat: continue for section in ["destination", "source", "static", "cgnat"]: if section in nat and "rule" in nat[section]: nat[section]["rule"] = self._merge_rule_list(nat[section]["rule"]) nat[section]["rule"].sort(key=lambda x: x.get("id", 0)) if "cgnat" in nat and "pool" in nat["cgnat"]: pool = nat["cgnat"]["pool"] for ptype in ["external", "internal"]: if ptype in pool: pool[ptype] = self._merge_pool_list(pool[ptype]) if nat_type == "nat64": for rule in nat.get("source", {}).get("rule", []): pools = rule.get("translation", {}).get("pool") if pools: rule["translation"]["pool"] = self._merge_rule_list(pools) rule["translation"]["pool"].sort(key=lambda x: x.get("id", 0)) return objs + + def _merge_range_list(self, ranges): + """Merge range entries by value, preserving seq.""" + merged = {} + for entry in ranges: + if isinstance(entry, dict): + key = entry["value"] + if key not in merged: + merged[key] = {"value": key} + if entry.get("seq"): + merged[key]["seq"] = entry["seq"] + else: + # fallback for plain strings during transition + merged[entry] = entry + return list(merged.values()) diff --git a/plugins/module_utils/network/vyos/rm_templates/nat.py b/plugins/module_utils/network/vyos/rm_templates/nat.py index abc7eb5b..18c853fa 100644 --- a/plugins/module_utils/network/vyos/rm_templates/nat.py +++ b/plugins/module_utils/network/vyos/rm_templates/nat.py @@ -1,1229 +1,1142 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function __metaclass__ = type import re from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( NetworkTemplate, ) class NatTemplate(NetworkTemplate): def __init__(self, lines=None, module=None): prefix = {"set": "set", "remove": "delete"} super(NatTemplate, self).__init__(lines=lines, tmplt=self, prefix=prefix, module=module) - # def parse(self): - # data = super(NatTemplate, self).parse() - # return self._normalize(data) - - # def _normalize(self, data): - # def convert_rules(section): - # if not section or "rule" not in section: - # return section - - # rules = section["rule"] - - # if isinstance(rules, dict): - # new_rules = [] - # for rule_id, rule_data in rules.items(): - # rule = rule_data.copy() - - # # normalize id - # try: - # rule["id"] = int(rule_id) - # except (ValueError, TypeError): - # rule["id"] = rule_id - - # new_rules.append(rule) - - # section["rule"] = sorted(new_rules, key=lambda x: x.get("id", 0)) - - # return section - - # if not data: - # return data - - # for nat_type in ["nat", "nat64", "nat66"]: - # if nat_type not in data: - # continue - - # nat = data[nat_type] - - # for block in ["destination", "source", "static"]: - # if block in nat: - # nat[block] = convert_rules(nat[block]) - - # # CGNAT rules - # if "cgnat" in nat and "rule" in nat["cgnat"]: - # rules = nat["cgnat"]["rule"] - # if isinstance(rules, list): - # for r in rules: - # if "id" in r: - # r["id"] = int(r["id"]) - - # return data - - # def _normalize(self, data): - # if not data: - # return data - - # def normalize_rules(rules): - # """Convert rules dict → sorted list with int IDs, or cast IDs in existing list.""" - # if isinstance(rules, dict): - # result = [] - # for rule_id, rule_data in rules.items(): - # rule = rule_data.copy() - # try: - # rule["id"] = int(rule_id) - # except (ValueError, TypeError): - # rule["id"] = rule_id - # result.append(rule) - # return sorted(result, key=lambda x: x.get("id", 0)) - - # if isinstance(rules, list): - # for rule in rules: - # if "id" in rule: - # try: - # rule["id"] = int(rule["id"]) - # except (ValueError, TypeError): - # pass - # return rules - - # return rules - - # for nat_type in ["nat", "nat64", "nat66"]: - # nat = data.get(nat_type) - # if not nat: - # continue - - # for block in ["destination", "source", "static", "cgnat"]: - # section = nat.get(block) - # if section and "rule" in section: - # section["rule"] = normalize_rules(section["rule"]) - - # return data - # fmt: off PARSERS = [ # # ------------------------- # CGNAT (keep explicit) # ------------------------- # { "name": "cgnat_log_allocation", "getval": re.compile( r""" ^set \s+nat \s+cgnat \s+log-allocation $""", re.VERBOSE, ), "setval": "nat cgnat log-allocation", "result": { "nat": { "cgnat": { "log_allocation": True, }, }, }, }, { "name": "cgnat_pool_external_range", "getval": re.compile( r""" ^set \s+nat \s+cgnat \s+pool \s+external \s+(?P\S+) \s+range \s+(?P\S+)(?:\s+seq\s+(?P\d+))? $""", re.VERBOSE, ), "setval": "nat cgnat pool external {{ name }} range {{ range }}{% if seq is defined %} seq {{ seq }}{% endif %}", "result": { "nat": { "cgnat": { "pool": { "external": [ { "name": "{{ name }}", - "range": ["{{ range }}"], - "seq": "{{ seq }}", + "range": [ + { + "value": "{{ range }}", + "seq": "{{ seq }}", + }, + ], }, ], }, }, }, }, }, { "name": "cgnat_pool_external_port_range", "getval": re.compile( r""" ^set \s+nat \s+cgnat \s+pool \s+external \s+(?P\S+) \s+external-port-range \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat cgnat pool external {{ name }} external-port-range {{ range }}", "result": { "nat": { "cgnat": { "pool": { "external": [ { "name": "{{ name }}", "external_port_range": "{{ range }}", }, ], }, }, }, }, }, { "name": "cgnat_pool_external_per_user", "getval": re.compile( r""" ^set \s+nat \s+cgnat \s+pool \s+external \s+(?P\S+) \s+per-user-limit \s+port \s+(?P\d+) $""", re.VERBOSE, ), "setval": "nat cgnat pool external {{ name }} per-user-limit port {{ limit }}", "result": { "nat": { "cgnat": { "pool": { "external": [ { "name": "{{ name }}", "per_user_limit": {"port": "{{ limit }}"}, }, ], }, }, }, }, }, { "name": "cgnat_pool_internal_range", "getval": re.compile( r""" ^set \s+nat \s+cgnat \s+pool \s+internal \s+(?P\S+) \s+range \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat cgnat pool internal {{ name }} range {{ range }}", "result": { "nat": { "cgnat": { "pool": { "internal": [ { "name": "{{ name }}", "range": ["{{ range }}"], }, ], }, }, }, }, }, { "name": "cgnat_rule_source_pool", "getval": re.compile( r""" ^set \s+nat \s+cgnat \s+rule \s+(?P\d+) \s+source \s+pool \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat cgnat rule {{ id }} source pool {{ pool }}", "result": { "nat": { "cgnat": { "rule": [ { "id": "{{ id }}", "source": {"pool": "{{ pool }}"}, }, ], }, }, }, }, { "name": "cgnat_rule_translation_pool", "getval": re.compile( r""" ^set \s+nat \s+cgnat \s+rule \s+(?P\d+) \s+translation \s+pool \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat cgnat rule {{ id }} translation pool {{ pool }}", "result": { "nat": { "cgnat": { "rule": [ { "id": "{{ id }}", "translation": {"pool": "{{ pool }}"}, }, ], }, }, }, }, # # ------------------------- # GENERIC NAT (destination/source/static) # ------------------------- # # description { "name": "nat_type_description", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+description \s+(?P.+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} description {{ description }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "description": "{{ description }}", }, ], }, }, }, }, # protocol { "name": "nat_type_protocol", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+protocol \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} protocol {{ protocol }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "protocol": "{{ protocol }}", }, ], }, }, }, }, # flags { "name": "nat_type_disable", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+disable $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} disable", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "disable": True, }, ], }, }, }, }, { "name": "nat_type_exclude", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+exclude $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} exclude", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "exclude": True, }, ], }, }, }, }, { "name": "nat_type_log", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+log $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} log", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "log": True, }, ], }, }, }, }, # address (destination/source) { "name": "nat_type_address", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+(?Pdestination|source) \s+address \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} {{ atype }} address {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "{{ atype }}": {"address": "{{ value }}"}, }, ], }, }, }, }, # prefix (destination/source) { "name": "nat_type_prefix", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+(?Pdestination|source) \s+prefix \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} {{ atype }} prefix {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "{{ atype }}": {"prefix": "{{ value }}"}, }, ], }, }, }, }, # fqdn { "name": "nat_type_fqdn", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+(?Pdestination|source) \s+fqdn \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} {{ atype }} fqdn {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "{{ atype }}": {"fqdn": "{{ value }}"}, }, ], }, }, }, }, # port { "name": "nat_type_port", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+(?Pdestination|source) \s+port \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} {{ atype }} port {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "{{ atype }}": {"port": "{{ value }}"}, }, ], }, }, }, }, # translation address { "name": "nat_type_translation_address", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+translation \s+address \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} translation address {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "translation": {"address": "{{ value }}"}, }, ], }, }, }, }, # translation port { "name": "nat_type_translation_port", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+translation \s+port \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} translation port {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "translation": {"port": "{{ value }}"}, }, ], }, }, }, }, { "name": "nat_inbound_interface_name", "getval": re.compile( r""" ^set \s+nat \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+inbound-interface \s+name \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat {{ type }} rule {{ id }} inbound-interface name {{ value }}", "result": { "nat": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "inbound_interface": {"name": "{{ value }}"}, }, ], }, }, }, }, { "name": "nat_inbound_interface_group", "getval": re.compile( r""" ^set \s+nat \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+inbound-interface \s+group \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat {{ type }} rule {{ id }} inbound-interface group {{ value }}", "result": { "nat": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "inbound_interface": {"group": "{{ value }}"}, }, ], }, }, }, }, { "name": "nat_static_inbound_interface", "getval": re.compile( r""" ^set \s+nat \s+static \s+rule \s+(?P\S+) \s+inbound-interface \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat static rule {{ id }} inbound-interface {{ value }}", "result": { "nat": { "static": { "rule": [ { "id": "{{ id }}", "inbound_interface": "{{ value }}", }, ], }, }, }, }, # NAT6X inbound interface { "name": "nat6x_inbound_interface", "getval": re.compile( r""" ^set \s+(?Pnat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+inbound-interface \s+name \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} inbound-interface name {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "inbound_interface": {"name": "{{ value }}"}, }, ], }, }, }, }, # outbound interface { "name": "nat_type_outbound_interface", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+outbound-interface \s+name \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface name {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "outbound_interface": {"name": "{{ value }}"}, }, ], }, }, }, }, { "name": "nat_type_outbound_interface_group", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source|static) \s+rule \s+(?P\S+) \s+outbound-interface \s+group \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface group {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "outbound_interface": {"group": "{{ value }}"}, }, ], }, }, }, }, { "name": "nat_type_address_group", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+(?Pdestination|source) \s+group \s+(?Paddress-group|domain-group|mac-group|network-group|port-group) \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} {{ atype }} group {{ gtype }} {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "{{ atype }}": { "{{ gtype | replace('-', '_') }}": "{{ value }}", }, }, ], }, }, }, }, # packet type { "name": "nat_type_packet_type", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+packet-type \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} packet-type {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "packet_type": "{{ value }}", }, ], }, }, }, }, # load balance backend { "name": "nat_type_lb_backend", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+load-balance \s+backend \s+(?P\S+) \s+weight \s+(?P\d+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance backend {{ ip }} weight {{ weight }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "load_balance": { "backend": { "ip": "{{ ip }}", "weight": "{{ weight }}", }, }, }, ], }, }, }, }, # load balance hash { "name": "nat_type_lb_hash", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+load-balance \s+hash \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance hash {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "load_balance": {"hash": "{{ value }}"}, }, ], }, }, }, }, # translation options { "name": "nat_type_translation_options", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+translation \s+options \s+(?Paddress-mapping|port-mapping) \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} translation options {{ opt }} {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "translation": { "{{ opt | replace(\"-\", \"_\") }}": "{{ value }}", }, }, ], }, }, }, }, # redirect port { "name": "nat_type_translation_redirect", "getval": re.compile( r""" ^set \s+(?Pnat|nat64|nat66) \s+(?Pdestination|source) \s+rule \s+(?P\S+) \s+translation \s+redirect \s+port \s+(?P\S+) $""", re.VERBOSE, ), "setval": "{{ nat }} {{ type }} rule {{ id }} translation redirect port {{ value }}", "result": { "{{ nat }}": { "{{ type }}": { "rule": [ { "id": "{{ id }}", "translation": { "redirect_port": "{{ value }}", }, }, ], }, }, }, }, { "name": "nat64_match_mark", "getval": re.compile( r""" ^set \s+nat64 \s+source \s+rule \s+(?P\S+) \s+match \s+mark \s+(?P\d+) $""", re.VERBOSE, ), "setval": "nat64 source rule {{ id }} match mark {{ mark }}", "result": { "nat64": { "source": { "rule": [ { "id": "{{ id }}", "match": {"mark": "{{ mark }}"}, }, ], }, }, }, }, { "name": "nat64_translation_pool_address", "getval": re.compile( r""" ^set \s+nat64 \s+source \s+rule \s+(?P\S+) \s+translation \s+pool \s+(?P\d+) \s+address \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} address {{ value }}", "result": { "nat64": { "source": { "rule": [ { "id": "{{ id }}", "translation": { "pool": [{"id": "{{ pool_id }}", "address": "{{ value }}"}], }, }, ], }, }, }, }, { "name": "nat64_translation_pool_description", "getval": re.compile( r""" ^set \s+nat64 \s+source \s+rule \s+(?P\S+) \s+translation \s+pool \s+(?P\d+) \s+description \s+(?P.+) $""", re.VERBOSE, ), "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} description {{ value }}", "result": { "nat64": { "source": { "rule": [ { "id": "{{ id }}", "translation": { "pool": [{"id": "{{ pool_id }}", "description": "{{ value }}"}], }, }, ], }, }, }, }, { "name": "nat64_translation_pool_disable", "getval": re.compile( r""" ^set \s+nat64 \s+source \s+rule \s+(?P\S+) \s+translation \s+pool \s+(?P\d+) \s+disable $""", re.VERBOSE, ), "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} disable", "result": { "nat64": { "source": { "rule": [ { "id": "{{ id }}", "translation": { "pool": [{"id": "{{ pool_id }}", "disable": True}], }, }, ], }, }, }, }, { "name": "nat64_translation_pool_port", "getval": re.compile( r""" ^set \s+nat64 \s+source \s+rule \s+(?P\S+) \s+translation \s+pool \s+(?P\d+) \s+port \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} port {{ value }}", "result": { "nat64": { "source": { "rule": [ { "id": "{{ id }}", "translation": { "pool": [{"id": "{{ pool_id }}", "port": "{{ value }}"}], }, }, ], }, }, }, }, { "name": "nat64_translation_pool_protocol", "getval": re.compile( r""" ^set \s+nat64 \s+source \s+rule \s+(?P\S+) \s+translation \s+pool \s+(?P\d+) \s+protocol \s+(?P\S+) $""", re.VERBOSE, ), "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} protocol {{ value }}", "result": { "nat64": { "source": { "rule": [ { "id": "{{ id }}", "translation": { "pool": [{"id": "{{ pool_id }}", "protocol": "{{ value }}"}], }, }, ], }, }, }, }, ] # fmt: on diff --git a/plugins/modules/vyos_nat.py b/plugins/modules/vyos_nat.py index 7b341b6c..698e66b2 100644 --- a/plugins/modules/vyos_nat.py +++ b/plugins/modules/vyos_nat.py @@ -1,782 +1,790 @@ #!/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_nat """ from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = """ module: vyos_nat version_added: 1.0.0 short_description: NAT resource module description: - This module manages NAT configuration on devices running VyOS. author: - Evgeny Molotkov (@omnom62) 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(network_cli). options: config: description: - The desired configuration for the NAT resource represented as a dictionary. type: dict suboptions: nat: type: dict description: Configuration for NAT rules. suboptions: cgnat: type: dict description: Configuration for Carrier Grade NAT (CGNAT). suboptions: log_allocation: type: bool description: Log CGNAT address allocations. pool: type: dict description: Configuration for CGNAT pools. suboptions: external: type: list elements: dict description: List of external NAT pools for CGNAT. suboptions: name: type: str required: true description: Name of the external NAT pool. external_port_range: type: str description: Port range to use for NAT translations in this external pool. per_user_limit: type: dict description: Per-user limit configuration for the external pool. suboptions: port: type: str description: Maximum number of ports allocated per user. range: type: list - elements: str - description: List of external IP addresses or prefixes in the pool. + elements: dict + description: List of external IP address ranges in the pool. + suboptions: + value: + type: str + required: true + description: IP address, prefix, or range (e.g. 203.0.113.0/24 or 203.0.113.1-203.0.113.60). + seq: + type: str + description: Optional sequence number for this range entry. internal: type: list elements: dict description: List of internal NAT pools for CGNAT. suboptions: name: type: str required: true description: Name of the internal NAT pool. range: type: list elements: str description: List of internal IP addresses or prefixes in the pool. rule: type: list elements: dict description: List of CGNAT rules. suboptions: id: type: int required: true description: Rule number for CGNAT. source: type: dict description: Source pool configuration for CGNAT translation. suboptions: pool: type: str description: Source pool name to use for CGNAT translation. translation: type: dict description: Translation pool configuration for CGNAT. suboptions: pool: type: str description: Translation pool name to use for CGNAT translation. destination: type: dict description: Configuration for destination NAT rules. suboptions: rule: type: list elements: dict description: List of destination NAT rules. suboptions: id: type: int required: true description: Rule number for destination NAT. description: type: str description: User-friendly description of the destination NAT rule. protocol: type: str description: Protocol to NAT (default all). packet_type: type: str description: Packet type to match. exclude: type: bool description: Exclude packets matching this rule from NAT. log: type: bool description: Log packets hitting this rule. disable: type: bool description: Disable this destination NAT rule. inbound_interface: type: dict description: Match inbound interface. suboptions: name: type: str description: Interface name to match. group: type: str description: Interface group to match. destination: type: dict description: Match criteria for destination NAT. suboptions: address: type: str description: IP address, subnet, or range to match. fqdn: type: str description: Fully qualified domain name to match. port: type: str description: Port number or range to match. address_group: type: str description: Address group name to match. domain_group: type: str description: Domain group name to match. mac_group: type: str description: MAC address group name to match. network_group: type: str description: Network group name to match. port_group: type: str description: Port group name to match. translation: type: dict description: Translation configuration for destination NAT. suboptions: address: type: str description: IP address or prefix to translate destination to. port: type: str description: Port number or range to translate destination port to. redirect_port: type: str description: Redirect to local port number. address_mapping: type: str choices: - random - persistent description: Address mapping mode for translation. port_mapping: type: str choices: - random - none description: Port mapping mode for translation. source: type: dict description: Configuration for source NAT rules. suboptions: rule: type: list elements: dict description: List of source NAT rules. suboptions: id: type: int required: true description: Rule number for source NAT. description: type: str description: User-friendly description of the source NAT rule. protocol: type: str description: Protocol to NAT (default all). packet_type: type: str description: Packet type to match. exclude: type: bool description: Exclude packets matching this rule from NAT. log: type: bool description: Log packets hitting this rule. disable: type: bool description: Disable this source NAT rule. outbound_interface: type: dict description: Match outbound interface. suboptions: name: type: str description: Interface name to match. group: type: str description: Interface group to match. destination: type: dict description: Destination match criteria for source NAT. suboptions: address: type: str description: IP address, subnet, or range to match. fqdn: type: str description: Fully qualified domain name to match. port: type: str description: Port number or range to match. address_group: type: str description: Address group name to match. domain_group: type: str description: Domain group name to match. mac_group: type: str description: MAC address group name to match. network_group: type: str description: Network group name to match. port_group: type: str description: Port group name to match. source: type: dict description: Source match criteria for source NAT. suboptions: address: type: str description: IP address, subnet, or range to match. fqdn: type: str description: Fully qualified domain name to match. port: type: str description: Port number or range to match. address_group: type: str description: Address group name to match. domain_group: type: str description: Domain group name to match. mac_group: type: str description: MAC address group name to match. network_group: type: str description: Network group name to match. port_group: type: str description: Port group name to match. translation: type: dict description: Translation configuration for source NAT. suboptions: address: type: str description: IP address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address. port: type: str description: Port number or range to translate source port to. address_mapping: type: str choices: - random - persistent description: Address mapping mode for translation. port_mapping: type: str choices: - random - none description: Port mapping mode for translation. static: type: dict description: Configuration for static one-to-one NAT rules. suboptions: rule: type: list elements: dict description: List of static NAT rules. suboptions: id: type: int required: true description: Rule number for static NAT. description: type: str description: User-friendly description of the static NAT rule. destination: type: dict description: Match criteria for static NAT. suboptions: address: type: str description: IP address, subnet, or range to match. inbound_interface: type: str description: Inbound interface that this static NAT rule applies to. log: type: bool description: Log packets hitting this static NAT rule. translation: type: dict description: Translation configuration for static NAT. suboptions: address: type: str description: IP address or prefix to translate to. nat64: type: dict description: Configuration for NAT64 (IPv6-to-IPv4) rules. suboptions: source: type: dict description: Configuration for NAT64 source rules. suboptions: rule: type: list elements: dict description: List of NAT64 source rules. suboptions: id: type: int required: true description: Rule number for NAT64 source rule (1-999999). description: type: str description: User-friendly description of the NAT64 source rule. disable: type: bool description: Disable this NAT64 source rule. match: type: dict description: Match criteria for NAT64 source rule. suboptions: mark: type: int description: Match on firewall mark value (1-2147483647). source: type: dict description: IPv6 source prefix to match for NAT64 translation. suboptions: prefix: type: str description: IPv6 source prefix to match (h:h:h:h:h:h:h:h/x). translation: type: dict description: Translation configuration for NAT64 source rule. suboptions: pool: type: list elements: dict description: List of translation pools for NAT64. suboptions: id: type: int required: true description: Pool number (1-999999). address: type: str description: IPv4 address or prefix for translation pool. description: type: str description: User-friendly description of the translation pool. disable: type: bool description: Disable this translation pool. port: type: str description: Port number or range for translation pool. protocol: type: str choices: - icmp - tcp - udp description: Protocol for this translation pool entry. nat66: type: dict description: Configuration for NAT66 (IPv6-to-IPv6) rules. suboptions: destination: type: dict description: Configuration for NAT66 destination rules. suboptions: rule: type: list elements: dict description: List of NAT66 destination rules. suboptions: id: type: int required: true description: Rule number for NAT66 destination rule. description: type: str description: User-friendly description of the NAT66 destination rule. destination: type: dict description: Match criteria for NAT66 destination rule. suboptions: address: type: str description: IPv6 address or prefix to match. port: type: str description: Port number or range to match. disable: type: bool description: Disable this NAT66 destination rule. exclude: type: bool description: Exclude packets matching this rule from NAT66. inbound_interface: type: dict description: Inbound interface to match for NAT66 destination rule. suboptions: name: type: str description: Interface name to match. log: type: bool description: Log packets hitting this NAT66 destination rule. protocol: type: str description: Protocol to match. source: type: dict description: Source match criteria for NAT66 destination rule. suboptions: address: type: str description: IPv6 source address or prefix to match. port: type: str description: Source port number or range to match. translation: type: dict description: Translation configuration for NAT66 destination rule. suboptions: address: type: str description: IPv6 address or prefix to translate destination to. port: type: str description: Port number or range to translate destination port to. source: type: dict description: Configuration for NAT66 source rules. suboptions: rule: type: list elements: dict description: List of NAT66 source rules. suboptions: id: type: int required: true description: Rule number for NAT66 source rule. description: type: str description: User-friendly description of the NAT66 source rule. destination: type: dict description: Destination match criteria for NAT66 source rule. suboptions: port: type: str description: Destination port number or range to match. prefix: type: str description: IPv6 destination prefix to match (h:h:h:h:h:h:h:h/x). disable: type: bool description: Disable this NAT66 source rule. exclude: type: bool description: Exclude packets matching this rule from NAT66. log: type: bool description: Log packets hitting this NAT66 source rule. outbound_interface: type: dict description: Outbound interface to match for NAT66 source rule. suboptions: name: type: str description: Interface name to match. protocol: type: str description: Protocol to match. source: type: dict description: Source match criteria for NAT66 source rule. suboptions: port: type: str description: Source port number or range to match. prefix: type: str description: IPv6 source prefix to match (h:h:h:h:h:h:h:h/x). translation: type: dict description: Translation configuration for NAT66 source rule. suboptions: address: type: str description: IPv6 address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address. port: type: str description: Port number or range to translate source port to. running_config: description: - This option is used only with state I(parsed). - The value of this option should be the output received from the VyOS device by executing the command B(show configuration commands | grep nat). - The state I(parsed) reads the configuration from C(show configuration commands | grep nat) and transforms it into Ansible structured data as per the module argspec. The value is then returned in the I(parsed) key within the result. - The states I(replaced) and I(overridden) have identical behaviour for this module. type: str state: description: - The state the configuration should be left in. type: str choices: - deleted - merged - overridden - replaced - gathered - rendered - parsed default: merged """ EXAMPLES = """ # Using merged - configure CGNAT - name: Merge CGNAT configuration vyos.vyos.vyos_nat: config: nat: cgnat: log_allocation: true pool: external: - name: ext-pool-1 external_port_range: "10000-20000" per_user_limit: port: "200" range: - 203.0.113.0/24 internal: - name: int-pool-1 range: - 10.0.0.0/24 rule: - id: 1 source: pool: int-pool-1 translation: pool: ext-pool-1 state: merged # Using merged - configure destination NAT - name: Merge destination NAT rule vyos.vyos.vyos_nat: config: nat: destination: rule: - id: 100 description: "Web server NAT" protocol: tcp log: true destination: address: 198.51.100.10 port: "80" translation: address: 192.168.1.10 port: "8080" state: merged # Using merged - configure source NAT - name: Merge source NAT rule vyos.vyos.vyos_nat: config: nat: source: rule: - id: 200 description: "Outbound masquerade" protocol: tcp log: true outbound_interface: name: eth0 translation: address: masquerade state: merged # Using merged - configure static NAT - name: Merge static NAT rule vyos.vyos.vyos_nat: config: nat: static: rule: - id: 300 description: "Static mapping" inbound_interface: eth2 destination: address: 198.51.100.20 translation: address: 192.168.1.20 log: true state: merged # Using merged - configure NAT64 - name: Merge NAT64 source rule vyos.vyos.vyos_nat: config: nat64: source: rule: - id: 10 description: "NAT64 example" source: prefix: 2001:db8::/96 match: mark: "100" translation: pool: - id: 1 address: 192.168.100.10 port: "1-65535" protocol: udp state: merged # Using merged - configure NAT66 - name: Merge NAT66 destination rule vyos.vyos.vyos_nat: config: nat66: destination: rule: - id: 20 description: "NAT66 DNAT" protocol: tcp inbound_interface: name: eth1 destination: address: 2001:db8::1 translation: address: 2001:db8:1::10 port: "8443" state: merged # Using gathered - name: Gather NAT config vyos.vyos.vyos_nat: state: gathered # Using deleted - name: Delete all NAT config vyos.vyos.vyos_nat: state: deleted # Using replaced - name: Replace NAT source rules vyos.vyos.vyos_nat: config: nat: source: rule: - id: 200 description: "Replaced outbound rule" translation: address: masquerade state: replaced # Using parsed - name: Parse NAT config from file vyos.vyos.vyos_nat: running_config: "{{ lookup('file', './nat_config.cfg') }}" state: parsed # Using rendered - name: Render NAT config offline vyos.vyos.vyos_nat: config: nat: source: rule: - id: 200 description: "Rendered rule" translation: address: masquerade state: rendered """ from ansible.module_utils.basic import AnsibleModule from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.nat.nat import ( NatArgs, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.nat.nat import ( Nat, ) def main(): """ Main entry point for module execution :returns: the result form module invocation """ module = AnsibleModule( argument_spec=NatArgs.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 = Nat(module).execute_module() module.exit_json(**result) if __name__ == "__main__": main()