diff options
| author | omnom62 <75066712+omnom62@users.noreply.github.com> | 2026-09-01 22:37:23 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 15:37:23 +0300 |
| commit | 497022e13c372ed68afe49aeafca66285e2f8001 (patch) | |
| tree | bb01d4dbe45e34a465244e2f89778012ee65a6cf | |
| parent | 61d27bc11d3f64ad4ad66ec42ff5ae83ccd30732 (diff) | |
| download | vyos.vyos-497022e13c372ed68afe49aeafca66285e2f8001.tar.gz vyos.vyos-497022e13c372ed68afe49aeafca66285e2f8001.zip | |
T8323: vyos_nat module (#451)
Add NAT module
45 files changed, 9090 insertions, 3 deletions
@@ -60,6 +60,7 @@ Name | Description [vyos.vyos.vyos_lldp_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_lldp_global_module.rst)|LLDP global resource module [vyos.vyos.vyos_lldp_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_lldp_interfaces_module.rst)|LLDP interfaces resource module [vyos.vyos.vyos_logging_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_logging_global_module.rst)|Logging resource module +[vyos.vyos.vyos_nat](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_nat_module.rst)|NAT resource module [vyos.vyos.vyos_ntp_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ntp_global_module.rst)|NTP global resource module [vyos.vyos.vyos_ospf_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ospf_interfaces_module.rst)|OSPF Interfaces Resource Module. [vyos.vyos.vyos_ospfv2](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ospfv2_module.rst)|OSPFv2 resource module diff --git a/changelogs/fragments/t8323-vyos_nat.yml b/changelogs/fragments/t8323-vyos_nat.yml new file mode 100644 index 00000000..ae71d4a9 --- /dev/null +++ b/changelogs/fragments/t8323-vyos_nat.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - vyos_nat - Add new module to support NAT configuration. diff --git a/docs/vyos.vyos.vyos_nat_module.rst b/docs/vyos.vyos.vyos_nat_module.rst new file mode 100644 index 00000000..7a8045ee --- /dev/null +++ b/docs/vyos.vyos.vyos_nat_module.rst @@ -0,0 +1,3723 @@ +.. _vyos.vyos.vyos_nat_module: + + +****************** +vyos.vyos.vyos_nat +****************** + +**NAT resource module** + + +Version added: 6.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module manages NAT configuration on devices running VyOS. + + + + +Parameters +---------- + +.. raw:: html + + <table border=0 cellpadding=0 class="documentation-table"> + <tr> + <th colspan="7">Parameter</th> + <th>Choices/<font color="blue">Defaults</font></th> + <th width="100%">Comments</th> + </tr> + <tr> + <td colspan="7"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>config</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>The desired configuration for the NAT resource represented as a dictionary.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="6"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>nat</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for NAT rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="5"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>cgnat</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for Carrier Grade NAT (CGNAT).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>log_allocation</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Log CGNAT address allocations.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>pool</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for CGNAT pools.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>external</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of external NAT pools for CGNAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>external_port_range</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port range to use for NAT translations in this external pool.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Name of the external NAT pool.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>per_user_limit</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Per-user limit configuration for the external pool.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Maximum number of ports allocated per user.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>range</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of external IP address ranges in the pool.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>seq</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Optional sequence number for this range entry.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>value</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address, prefix, or range (e.g. 203.0.113.0/24 or 203.0.113.1-203.0.113.60).</div> + </td> + </tr> + + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>internal</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of internal NAT pools for CGNAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Name of the internal NAT pool.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>range</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=string</span> + </div> + </td> + <td> + </td> + <td> + <div>List of internal IP addresses or prefixes in the pool.</div> + </td> + </tr> + + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rule</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of CGNAT rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>id</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Rule number for CGNAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>source</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Source pool configuration for CGNAT translation.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>pool</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Source pool name to use for CGNAT translation.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>translation</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Translation pool configuration for CGNAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>pool</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Translation pool name to use for CGNAT translation.</div> + </td> + </tr> + + + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="5"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>destination</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for destination NAT rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rule</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of destination NAT rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>description</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>User-friendly description of the destination NAT rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>destination</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Match criteria for destination NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address, subnet, or range to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Address group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>domain_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Domain group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>fqdn</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Fully qualified domain name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>mac_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>MAC address group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>network_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Network group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port number or range to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port group name to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>disable</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Disable this destination NAT rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>exclude</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Exclude packets matching this rule from NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>id</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Rule number for destination NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>inbound_interface</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Match inbound interface.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Interface group to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Interface name to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>load_balance</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Load balancing configuration for this NAT rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>backend</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of backends to load-balance across. Weights should sum to 100.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ip</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address of the backend translation target.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>weight</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + </div> + </td> + <td> + </td> + <td> + <div>Relative weight (1-100) for this backend's share of load-balanced traffic.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>hash</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>source-address</li> + <li>destination-address</li> + <li>source-port</li> + <li>destination-port</li> + <li>random</li> + </ul> + </td> + <td> + <div>Fields to hash on for load balancing. Mutually exclusive with <em>translation.address</em>.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>log</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Log packets hitting this rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>packet_type</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Packet type to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>protocol</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Protocol to NAT (default all).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>translation</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Translation configuration for destination NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address or prefix to translate destination to.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address_mapping</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>random</li> + <li>persistent</li> + </ul> + </td> + <td> + <div>Address mapping mode for translation.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port number or range to translate destination port to.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port_mapping</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>random</li> + <li>none</li> + </ul> + </td> + <td> + <div>Port mapping mode for translation.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>redirect_port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Redirect to local port number.</div> + </td> + </tr> + + + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="5"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>source</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for source NAT rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rule</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of source NAT rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>description</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>User-friendly description of the source NAT rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>destination</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Destination match criteria for source NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address, subnet, or range to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Address group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>domain_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Domain group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>fqdn</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Fully qualified domain name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>mac_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>MAC address group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>network_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Network group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port number or range to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port group name to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>disable</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Disable this source NAT rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>exclude</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Exclude packets matching this rule from NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>id</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Rule number for source NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>load_balance</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Load balancing configuration for this NAT rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>backend</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of backends to load-balance across. Weights should sum to 100.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ip</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address of the backend translation target.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>weight</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + </div> + </td> + <td> + </td> + <td> + <div>Relative weight (1-100) for this backend's share of load-balanced traffic.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>hash</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>source-address</li> + <li>destination-address</li> + <li>source-port</li> + <li>destination-port</li> + <li>random</li> + </ul> + </td> + <td> + <div>Fields to hash on for load balancing. Mutually exclusive with <em>translation.address</em>.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>log</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Log packets hitting this rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>outbound_interface</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Match outbound interface.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Interface group to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Interface name to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>packet_type</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Packet type to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>protocol</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Protocol to NAT (default all).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>source</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Source match criteria for source NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address, subnet, or range to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Address group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>domain_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Domain group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>fqdn</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Fully qualified domain name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>mac_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>MAC address group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>network_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Network group name to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port number or range to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port group name to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>translation</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Translation configuration for source NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address_mapping</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>random</li> + <li>persistent</li> + </ul> + </td> + <td> + <div>Address mapping mode for translation.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port number or range to translate source port to.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port_mapping</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>random</li> + <li>none</li> + </ul> + </td> + <td> + <div>Port mapping mode for translation.</div> + </td> + </tr> + + + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="5"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>static</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for static one-to-one NAT rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rule</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of static NAT rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>description</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>User-friendly description of the static NAT rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>destination</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Match criteria for static NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address, subnet, or range to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>id</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Rule number for static NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>inbound_interface</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Inbound interface that this static NAT rule applies to.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>log</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Log packets hitting this static NAT rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>translation</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Translation configuration for static NAT.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP address or prefix to translate to.</div> + </td> + </tr> + + + + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="6"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>nat64</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for NAT64 (IPv6-to-IPv4) rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="5"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>source</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for NAT64 source rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rule</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of NAT64 source rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>description</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>User-friendly description of the NAT64 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>disable</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Disable this NAT64 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>id</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Rule number for NAT64 source rule (1-999999).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>match</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Match criteria for NAT64 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>mark</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + </div> + </td> + <td> + </td> + <td> + <div>Match on firewall mark value (1-2147483647).</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>source</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 source prefix to match for NAT64 translation.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>prefix</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 source prefix to match (h:h:h:h:h:h:h:h/x).</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>translation</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Translation configuration for NAT64 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>pool</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of translation pools for NAT64.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv4 address or prefix for translation pool.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>description</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>User-friendly description of the translation pool.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>disable</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Disable this translation pool.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>id</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Pool number (1-999999).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port number or range for translation pool.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>protocol</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>icmp</li> + <li>tcp</li> + <li>udp</li> + </ul> + </td> + <td> + <div>Protocol for this translation pool entry.</div> + </td> + </tr> + + + + + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="6"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>nat66</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for NAT66 (IPv6-to-IPv6) rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="5"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>destination</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for NAT66 destination rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rule</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of NAT66 destination rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>description</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>User-friendly description of the NAT66 destination rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>destination</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Match criteria for NAT66 destination rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 address or prefix to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port number or range to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>disable</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Disable this NAT66 destination rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>exclude</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Exclude packets matching this rule from NAT66.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>id</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Rule number for NAT66 destination rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>inbound_interface</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Inbound interface to match for NAT66 destination rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Interface name to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>log</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Log packets hitting this NAT66 destination rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>protocol</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Protocol to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>source</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Source match criteria for NAT66 destination rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 source address or prefix to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Source port number or range to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>translation</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Translation configuration for NAT66 destination rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 address or prefix to translate destination to.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port number or range to translate destination port to.</div> + </td> + </tr> + + + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="5"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>source</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Configuration for NAT66 source rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rule</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of NAT66 source rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>description</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>User-friendly description of the NAT66 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>destination</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Destination match criteria for NAT66 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Destination port number or range to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>prefix</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 destination prefix to match (h:h:h:h:h:h:h:h/x).</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>disable</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Disable this NAT66 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>exclude</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Exclude packets matching this rule from NAT66.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>id</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Rule number for NAT66 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>log</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Log packets hitting this NAT66 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>outbound_interface</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Outbound interface to match for NAT66 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Interface name to match.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>protocol</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Protocol to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>source</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Source match criteria for NAT66 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Source port number or range to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>prefix</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 source prefix to match (h:h:h:h:h:h:h:h/x).</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>translation</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Translation configuration for NAT66 source rule.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Port number or range to translate source port to.</div> + </td> + </tr> + + + + + + <tr> + <td colspan="7"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>running_config</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>This option is used only with state <em>parsed</em>.</div> + <div>The value of this option should be the output received from the VyOS device by executing the command <b>show configuration commands | match 'nat'</b>.</div> + <div>The state <em>parsed</em> reads the configuration from <code>show configuration commands | match 'nat'</code> and transforms it into Ansible structured data as per the module argspec. The value is then returned in the <em>parsed</em> key within the result.</div> + <div>The state <em>replaced</em> replaces only the provided configuration, while <em>overridden</em> removes any existing NAT configuration not specified in <em>config</em>.</div> + </td> + </tr> + <tr> + <td colspan="7"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>state</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>deleted</li> + <li><div style="color: blue"><b>merged</b> ←</div></li> + <li>overridden</li> + <li>replaced</li> + <li>gathered</li> + <li>rendered</li> + <li>parsed</li> + </ul> + </td> + <td> + <div>The state the configuration should be left in.</div> + </td> + </tr> + </table> + <br/> + + +Notes +----- + +.. note:: + - Tested against VyOS 1.4.3 and 1.5.0. + - 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: + - value: 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 replaced - replace specific NAT rules + - name: Replace destination NAT rule + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Replaced web server NAT" + protocol: tcp + destination: + address: 198.51.100.10 + port: "443" + translation: + address: 192.168.1.10 + port: "8443" + state: replaced + + # Using overridden - override entire NAT configuration + - name: Override entire NAT configuration + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Only rule after override" + protocol: tcp + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: overridden + + # Using deleted - delete all NAT configuration + - name: Delete all NAT configuration + vyos.vyos.vyos_nat: + state: deleted + + # Using deleted - delete specific NAT rules + - name: Delete specific NAT rules + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + source: + rule: + - id: 200 + nat64: + source: + rule: + - id: 10 + state: deleted + + # Using gathered + - name: Gather NAT configuration from device + vyos.vyos.vyos_nat: + state: gathered + + # Using rendered + - name: Render NAT configuration offline + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Rendered rule" + protocol: tcp + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: rendered + + # Using parsed + - name: Parse NAT configuration from file + vyos.vyos.vyos_nat: + running_config: "{{ lookup('file', './nat_config.cfg') }}" + state: parsed + + + +Return Values +------------- +Common return values are documented `here <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module: + +.. raw:: html + + <table border=0 cellpadding=0 class="documentation-table"> + <tr> + <th colspan="1">Key</th> + <th>Returned</th> + <th width="100%">Description</th> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>after</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td>when changed</td> + <td> + <div>The resulting configuration after module execution.</div> + <br/> + <div style="font-size: smaller"><b>Sample:</b></div> + <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>before</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td> + <td> + <div>The configuration prior to the module execution.</div> + <br/> + <div style="font-size: smaller"><b>Sample:</b></div> + <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>commands</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + </div> + </td> + <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td> + <td> + <div>The set of commands pushed to the remote device.</div> + <br/> + <div style="font-size: smaller"><b>Sample:</b></div> + <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">["set nat destination rule 100 description 'Web server NAT'", 'set nat destination rule 100 protocol tcp', 'set nat destination rule 100 inbound-interface name eth2', 'set nat destination rule 100 destination address 198.51.100.10', 'set nat destination rule 100 translation address 192.168.1.10', 'delete nat source rule 200']</div> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>gathered</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td>when <em>state</em> is <code>gathered</code></td> + <td> + <div>Facts about the network resource gathered from the remote device as structured data.</div> + <br/> + <div style="font-size: smaller"><b>Sample:</b></div> + <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>parsed</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td>when <em>state</em> is <code>parsed</code></td> + <td> + <div>The device native config provided in <em>running_config</em> option parsed into structured data as per module argspec.</div> + <br/> + <div style="font-size: smaller"><b>Sample:</b></div> + <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>rendered</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + </div> + </td> + <td>when <em>state</em> is <code>rendered</code></td> + <td> + <div>The provided configuration in the task rendered in device-native format (offline).</div> + <br/> + <div style="font-size: smaller"><b>Sample:</b></div> + <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">["set nat destination rule 100 description 'Web server NAT'", 'set nat destination rule 100 protocol tcp', 'set nat destination rule 100 inbound-interface name eth2', 'set nat destination rule 100 destination address 198.51.100.10', 'set nat destination rule 100 translation address 192.168.1.10']</div> + </td> + </tr> + </table> + <br/><br/> + + +Status +------ + + +Authors +~~~~~~~ + +- Evgeny Molotkov (@omnom62) diff --git a/meta/runtime.yml b/meta/runtime.yml index c1065dbd..093e129d 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -46,6 +46,8 @@ plugin_routing: redirect: vyos.vyos.vyos_logging_global ntp_global: redirect: vyos.vyos.vyos_ntp_global + nat: + redirect: vyos.vyos.vyos_nat ospfv2: redirect: vyos.vyos.vyos_ospfv2 ospfv3: diff --git a/plugins/action/nat.py b/plugins/action/nat.py new file mode 120000 index 00000000..331a791f --- /dev/null +++ b/plugins/action/nat.py @@ -0,0 +1 @@ +vyos.py
\ No newline at end of file diff --git a/plugins/module_utils/network/vyos/argspec/nat/__init__.py b/plugins/module_utils/network/vyos/argspec/nat/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/nat/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/nat/nat.py b/plugins/module_utils/network/vyos/argspec/nat/nat.py new file mode 100644 index 00000000..6f81c53c --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/nat/nat.py @@ -0,0 +1,652 @@ +# -*- coding: utf-8 -*- +# 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", + "options": { + "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": "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", + ], + }, + }, + }, + "load_balance": { + "type": "dict", + "options": { + "backend": { + "type": "list", + "elements": "dict", + "options": { + "ip": {"type": "str"}, + "weight": {"type": "int"}, + }, + }, + "hash": { + "type": "list", + "elements": "str", + "choices": [ + "source-address", + "destination-address", + "source-port", + "destination-port", + "random", + ], + }, + }, + }, + }, + }, + }, + }, + "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", + ], + }, + }, + }, + "load_balance": { + "type": "dict", + "options": { + "backend": { + "type": "list", + "elements": "dict", + "options": { + "ip": {"type": "str"}, + "weight": {"type": "int"}, + }, + }, + "hash": { + "type": "list", + "elements": "str", + "choices": [ + "source-address", + "destination-address", + "source-port", + "destination-port", + "random", + ], + }, + }, + }, + }, + }, + }, + }, + "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/config/nat/__init__.py b/plugins/module_utils/network/vyos/config/nat/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/nat/__init__.py diff --git a/plugins/module_utils/network/vyos/config/nat/nat.py b/plugins/module_utils/network/vyos/config/nat/nat.py new file mode 100644 index 00000000..a2800914 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/nat/nat.py @@ -0,0 +1,577 @@ +# -*- coding: utf-8 -*- +# 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 + +from copy import deepcopy + +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.facts.facts import Facts +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.nat import ( + NatTemplate, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine + + +class Nat(ResourceModule): + """The vyos_nat config class""" + + def __init__(self, module): + super(Nat, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="nat", + tmplt=NatTemplate(), + ) + self.parsers = [] + + def execute_module(self): + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + wantd = deepcopy(self.want) + haved = deepcopy(self.have) + + if self.state == "merged": + wantd = combine(haved, wantd, recursive=True) + + if self.state == "deleted": + if not wantd: + for nat_type in haved: + self.commands.append(f"delete {nat_type}") + return + self._list_to_dict(wantd) + self._list_to_dict(haved) + self._delete_nat_objects(wantd, haved, only_missing=False) + return + + self._list_to_dict(wantd) + self._list_to_dict(haved) + + if self.state == "replaced": + self._delete_nat_objects(wantd, haved, only_missing=False) + self._set_commands(wantd, haved) + elif self.state == "overridden": + self._delete_nat_objects(wantd, haved, only_missing=True) + self._delete_nat_objects(wantd, haved, only_missing=False) + self._set_commands(wantd, haved) + else: + self._set_commands(wantd, haved) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------------- + # List → keyed dict conversion + # ------------------------------------------------------------------------- + + def _list_to_dict(self, config): + nat = config.get("nat", {}) + cgnat = nat.get("cgnat", {}) + + pool = cgnat.get("pool", {}) + for ptype in ("external", "internal"): + entries = pool.get(ptype) + if isinstance(entries, list): + pool[ptype] = {item["name"]: item for item in entries} + + rules = cgnat.get("rule") + if isinstance(rules, list): + cgnat["rule"] = {r["id"]: r for r in rules} + + for section in ("destination", "source", "static"): + rules = nat.get(section, {}).get("rule") + if isinstance(rules, list): + nat[section]["rule"] = {r["id"]: r for r in rules} + + nat64 = config.get("nat64", {}) + rules = nat64.get("source", {}).get("rule") + if isinstance(rules, list): + nat64["source"]["rule"] = {r["id"]: r for r in rules} + for rule in nat64["source"]["rule"].values(): + pools = rule.get("translation", {}).get("pool") + if isinstance(pools, list): + rule["translation"]["pool"] = {p["id"]: p for p in pools} + + nat66 = config.get("nat66", {}) + for section in ("destination", "source"): + rules = nat66.get(section, {}).get("rule") + if isinstance(rules, list): + nat66[section]["rule"] = {r["id"]: r for r in rules} + + # ------------------------------------------------------------------------- + # Top-level dispatch + # ------------------------------------------------------------------------- + + def _set_commands(self, wantd, haved): + self._compare_cgnat_global(wantd, haved) + self._compare_cgnat_pools(wantd, haved) + self._compare_cgnat_rules(wantd, haved) + + for section in ("destination", "source", "static"): + self._compare_nat_rules("nat", section, wantd, haved) + + self._compare_nat_rules("nat64", "source", wantd, haved) + + for section in ("destination", "source"): + self._compare_nat_rules("nat66", section, wantd, haved) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------------- + # Delete helpers + # ------------------------------------------------------------------------- + + def _delete_nat_objects(self, wantd, haved, only_missing=False): + """ + Generate delete commands for NAT objects. + only_missing=False: delete objects present in both want and have (when different) + only_missing=True: delete objects present in have but absent from want + """ + for nat_type in haved: + want_nat = wantd.get(nat_type, {}) + have_nat = haved[nat_type] + + if only_missing and nat_type not in wantd: + self.commands.append(f"delete {nat_type}") + continue + + for section in have_nat: + want_section = want_nat.get(section, {}) + have_section = have_nat[section] + + if only_missing and section not in want_nat: + self.commands.append( + f"delete {nat_type} {section.replace('_', '-')}", + ) + continue + + if section == "cgnat": + for pool_type in ("external", "internal"): + want_pools = want_section.get("pool", {}).get(pool_type, {}) + have_pools = have_section.get("pool", {}).get(pool_type, {}) + for name in have_pools: + if only_missing and name not in want_pools: + self.commands.append( + f"delete {nat_type} cgnat pool {pool_type} {name}", + ) + + elif not only_missing and name in want_pools: + if self.state == "deleted" or want_pools[name] != have_pools[name]: + self.commands.append( + f"delete {nat_type} cgnat pool {pool_type} {name}", + ) + want_rules = want_section.get("rule", {}) + have_rules = have_section.get("rule", {}) + for rid in have_rules: + if only_missing and rid not in want_rules: + self.commands.append(f"delete {nat_type} cgnat rule {rid}") + + elif not only_missing and rid in want_rules: + if self.state == "deleted" or want_rules[rid] != have_rules[rid]: + self.commands.append(f"delete {nat_type} cgnat rule {rid}") + else: + want_rules = want_section.get("rule", {}) + have_rules = have_section.get("rule", {}) + cli_section = section.replace("_", "-") + for rid in have_rules: + if only_missing and rid not in want_rules: + self.commands.append( + f"delete {nat_type} {cli_section} rule {rid}", + ) + + elif not only_missing and rid in want_rules: + if self.state == "deleted" or want_rules[rid] != have_rules[rid]: + self.commands.append( + f"delete {nat_type} {cli_section} rule {rid}", + ) + + # ------------------------------------------------------------------------- + # CGNAT + # ------------------------------------------------------------------------- + + def _compare_cgnat_global(self, wantd, haved): + if self.state in ("replaced", "overridden") and not wantd.get("nat", {}).get("cgnat"): + return + w = wantd.get("nat", {}).get("cgnat", {}).get("log_allocation") + h = haved.get("nat", {}).get("cgnat", {}).get("log_allocation") + if bool(w) != bool(h): + self.addcmd( + {"nat": {"cgnat": {"log_allocation": True}}}, + "cgnat_log_allocation", + not bool(w), + ) + + def _compare_cgnat_pools(self, wantd, haved): + want_ext = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {}) + have_ext = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {}) + want_int = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {}) + have_int = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {}) + + scope = self.state in ("replaced", "overridden") + ext_names = set(want_ext) if scope else set(want_ext) | set(have_ext) + int_names = set(want_int) if scope else set(want_int) | set(have_int) + + for name in ext_names: + w = want_ext.get(name, {}) + h = have_ext.get(name, {}) + if scope and w != h: + h = {} + self._compare_external_pool(name, w, h) + + for name in int_names: + w = want_int.get(name, {}) + h = have_int.get(name, {}) + if scope and w != h: + h = {} + self._compare_internal_pool(name, w, h) + + def _compare_external_pool(self, name, want, have): + w = want.get("external_port_range") + h = have.get("external_port_range") + if w != h: + if w: + self.addcmd({"name": name, "range": w}, "cgnat_pool_external_port_range", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"name": name, "range": h}, "cgnat_pool_external_port_range", True) + + w = want.get("per_user_limit", {}).get("port") + h = have.get("per_user_limit", {}).get("port") + if w != h: + if w: + self.addcmd({"name": name, "limit": w}, "cgnat_pool_external_per_user", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"name": name, "limit": h}, "cgnat_pool_external_per_user", True) + + want_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in want.get("range", [])} + have_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in have.get("range", [])} + for val, rng in want_ranges.items(): + existing = have_ranges.get(val) + if existing is None or existing != rng: + seq = rng.get("seq") if isinstance(rng, dict) else None + self.addcmd( + {"name": name, "range": val, "seq": seq}, + "cgnat_pool_external_range", + False, + ) + + if self.state in ("replaced", "overridden"): + for val in have_ranges: + if val not in want_ranges: + self.addcmd({"name": name, "range": val}, "cgnat_pool_external_range", True) + + def _compare_internal_pool(self, name, want, have): + want_ranges = set(want.get("range", [])) + have_ranges = set(have.get("range", [])) + + for rng in want_ranges - have_ranges: + self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", False) + + if self.state in ("replaced", "overridden"): + for rng in have_ranges - want_ranges: + self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", True) + + def _compare_cgnat_rules(self, wantd, haved): + want_rules = wantd.get("nat", {}).get("cgnat", {}).get("rule", {}) + have_rules = haved.get("nat", {}).get("cgnat", {}).get("rule", {}) + + rids = ( + set(want_rules) + if self.state in ("replaced", "overridden") + else set(want_rules) | set(have_rules) + ) + + for rid in rids: + w = want_rules.get(rid, {}) + h = have_rules.get(rid, {}) + + if self.state in ("replaced", "overridden") and w != h: + h = {} + + w_src = w.get("source", {}).get("pool") + h_src = h.get("source", {}).get("pool") + if w_src != h_src: + if w_src: + self.addcmd({"id": rid, "pool": w_src}, "cgnat_rule_source_pool", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"id": rid, "pool": h_src}, "cgnat_rule_source_pool", True) + + w_tr = w.get("translation", {}).get("pool") + h_tr = h.get("translation", {}).get("pool") + if w_tr != h_tr: + if w_tr: + self.addcmd({"id": rid, "pool": w_tr}, "cgnat_rule_translation_pool", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"id": rid, "pool": h_tr}, "cgnat_rule_translation_pool", True) + + # ------------------------------------------------------------------------- + # NAT / NAT64 / NAT66 rules + # ------------------------------------------------------------------------- + + def _compare_nat_rules(self, nat_type, section, wantd, haved): + want_rules = wantd.get(nat_type, {}).get(section, {}).get("rule", {}) + have_rules = haved.get(nat_type, {}).get(section, {}).get("rule", {}) + + rids = ( + set(want_rules) + if self.state in ("replaced", "overridden") + else set(want_rules) | set(have_rules) + ) + + for rid in rids: + w = want_rules.get(rid, {}) + h = have_rules.get(rid, {}) + if self.state in ("replaced", "overridden") and w != h: + h = {} + if w == h and self.state != "rendered": + continue + self._compare_rule(nat_type, section, rid, w, h) + + def _compare_rule(self, nat_type, section, rid, want, have): + ctx = {"nat": nat_type, "type": section, "id": rid} + + want_lb = want.get("load_balance") or {} + have_lb = have.get("load_balance") or {} + want_trans_addr = (want.get("translation") or {}).get("address") + have_trans_addr = (have.get("translation") or {}).get("address") + if want_lb and want_trans_addr is not None: + self._module.fail_json( + msg="translation.address and load_balance are mutually exclusive", + ) + if self.state == "merged": + if want_lb and have_trans_addr is not None: + self._module.fail_json( + msg=( + "Cannot add load_balance to a rule that already has translation.address with " + "state=merged; use state=replaced or state=overridden" + ), + ) + if want_trans_addr is not None and have_lb: + self._module.fail_json( + msg=( + "Cannot add translation.address to a rule that already has load_balance with " + "state=merged; use state=replaced or state=overridden" + ), + ) + + for field in set(want) | set(have): + if field == "inbound_interface": + continue + val = want.get(field) if field in want else have.get(field) + if isinstance(val, bool): + self._cmp_bool(want, have, field, ctx, f"nat_type_{field}") + elif isinstance(val, str): + self._cmp_scalar(want, have, field, ctx, f"nat_type_{field}") + + self._cmp_interface(want, have, ctx, nat_type, section) + self._cmp_outbound_interface(want, have, ctx) + for atype in ("destination", "source"): + self._cmp_addr_sub(want, have, atype, ctx) + self._cmp_translation(want, have, ctx) + self._cmp_match_mark(want, have, ctx) + self._cmp_nat64_pools(want, have, ctx) + self._cmp_load_balance(want, have, ctx) + + # ------------------------------------------------------------------------- + # Field-level helpers + # ------------------------------------------------------------------------- + + def _cmp_scalar(self, want, have, field, ctx, parser): + w = want.get(field) + h = have.get(field) + if w != h: + if w is not None: + self.addcmd(dict(ctx, **{field: w}), parser, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, **{field: h}), parser, True) + + def _cmp_bool(self, want, have, field, ctx, parser): + w = bool(want.get(field)) + h = bool(have.get(field)) + if w != h: + if w: + self.addcmd(dict(ctx), parser, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx), parser, True) + + def _cmp_interface(self, want, have, ctx, nat_type, section): + iface_w = want.get("inbound_interface") + iface_h = have.get("inbound_interface") + if iface_w == iface_h: + return + + if nat_type == "nat" and section == "static": + if iface_w: + self.addcmd(dict(ctx, value=iface_w), "nat_static_inbound_interface", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h), "nat_static_inbound_interface", True) + return + + iface_w = iface_w or {} + iface_h = iface_h or {} + + if nat_type == "nat": + parser_name = "nat_inbound_interface_name" + parser_group = "nat_inbound_interface_group" + else: + parser_name = "nat6x_inbound_interface" + parser_group = "nat6x_inbound_interface" + + if iface_w.get("name") != iface_h.get("name"): + if iface_w.get("name"): + self.addcmd(dict(ctx, value=iface_w["name"]), parser_name, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h["name"]), parser_name, True) + + if nat_type == "nat" and iface_w.get("group") != iface_h.get("group"): + if iface_w.get("group"): + self.addcmd(dict(ctx, value=iface_w["group"]), parser_group, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h["group"]), parser_group, True) + + def _cmp_outbound_interface(self, want, have, ctx): + iface_w = want.get("outbound_interface") or {} + iface_h = have.get("outbound_interface") or {} + + if iface_w.get("name") != iface_h.get("name"): + if iface_w.get("name"): + self.addcmd(dict(ctx, value=iface_w["name"]), "nat_type_outbound_interface", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h["name"]), "nat_type_outbound_interface", True) + + if iface_w.get("group") != iface_h.get("group"): + if iface_w.get("group"): + self.addcmd( + dict(ctx, value=iface_w["group"]), + "nat_type_outbound_interface_group", + False, + ) + elif self.state in ("replaced", "overridden"): + self.addcmd( + dict(ctx, value=iface_h["group"]), + "nat_type_outbound_interface_group", + True, + ) + + def _cmp_addr_sub(self, want, have, atype, ctx): + sub_w = want.get(atype) or {} + sub_h = have.get(atype) or {} + if sub_w == sub_h: + return + + changed = {k: v for k, v in sub_w.items() if sub_h.get(k) != v} + removed = { + k: v + for k, v in sub_h.items() + if k not in sub_w and self.state in ("replaced", "overridden") + } + + if changed: + self.addcmd(dict(ctx, atype=atype, sub=changed), "nat_type_address", False) + if removed: + self.addcmd(dict(ctx, atype=atype, sub=removed), "nat_type_address", True) + + def _cmp_translation(self, want, have, ctx): + trans_w = want.get("translation") or {} + trans_h = have.get("translation") or {} + if trans_w == trans_h: + return + + changed = {k: v for k, v in trans_w.items() if k != "pool" and trans_h.get(k) != v} + removed = { + k: v + for k, v in trans_h.items() + if k != "pool" and k not in trans_w and self.state in ("replaced", "overridden") + } + + if changed: + self.addcmd(dict(ctx, translation=changed), "nat_type_translation_address", False) + if removed: + self.addcmd(dict(ctx, translation=removed), "nat_type_translation_address", True) + + def _cmp_match_mark(self, want, have, ctx): + w = want.get("match", {}).get("mark") + h = have.get("match", {}).get("mark") + if w != h: + if w is not None: + self.addcmd(dict(ctx, mark=w), "nat64_match_mark", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, mark=h), "nat64_match_mark", True) + + def _cmp_nat64_pools(self, want, have, ctx): + want_pools = want.get("translation", {}).get("pool", {}) + have_pools = have.get("translation", {}).get("pool", {}) + + if isinstance(want_pools, list): + want_pools = {p["id"]: p for p in want_pools} + if isinstance(have_pools, list): + have_pools = {p["id"]: p for p in have_pools} + + for pid in set(want_pools) | set(have_pools): + wp = want_pools.get(pid, {}) + hp = have_pools.get(pid, {}) + + if wp == hp: + continue + + changed = {k: v for k, v in wp.items() if k != "id" and hp.get(k) != v} + removed = { + k: v + for k, v in hp.items() + if k != "id" and k not in wp and self.state in ("replaced", "overridden") + } + + if changed: + self.addcmd( + dict(ctx, pool_id=pid, pool=changed), + "nat64_translation_pool", + False, + ) + if removed: + self.addcmd( + dict(ctx, pool_id=pid, pool=removed), + "nat64_translation_pool", + True, + ) + + def _cmp_load_balance(self, want, have, ctx): + lb_w = want.get("load_balance") or {} + lb_h = have.get("load_balance") or {} + + want_hash = set(lb_w.get("hash") or []) + have_hash = set(lb_h.get("hash") or []) + + for h in want_hash - have_hash: + self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", False) + if self.state in ("replaced", "overridden"): + for h in have_hash - want_hash: + self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", True) + + want_backends = lb_w.get("backend", []) + have_backends = lb_h.get("backend", []) + if isinstance(want_backends, list): + want_backends = {b["ip"]: b for b in want_backends} + if isinstance(have_backends, list): + have_backends = {b["ip"]: b for b in have_backends} + + for ip in set(want_backends) | set(have_backends): + wb = want_backends.get(ip, {}) + hb = have_backends.get(ip, {}) + if wb == hb: + continue + if wb: + weight = wb.get("weight") + if weight is None: + self._module.fail_json(msg="load_balance.backend entries require 'weight'") + self.addcmd(dict(ctx, ip=ip, weight=weight), "nat_type_lb_backend", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, ip=ip, weight=hb.get("weight")), "nat_type_lb_backend", True) diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py index 69cb0d0b..92c0f97a 100644 --- a/plugins/module_utils/network/vyos/facts/facts.py +++ b/plugins/module_utils/network/vyos/facts/facts.py @@ -59,6 +59,9 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lldp_ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.logging_global.logging_global import ( Logging_globalFacts, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.nat.nat import ( + NatFacts, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ntp_global.ntp_global import ( Ntp_globalFacts, ) @@ -112,6 +115,7 @@ FACT_RESOURCE_SUBSETS = dict( prefix_lists=Prefix_listsFacts, logging_global=Logging_globalFacts, ntp_global=Ntp_globalFacts, + nat=NatFacts, snmp_server=Snmp_serverFacts, hostname=HostnameFacts, vrf=VrfFacts, diff --git a/plugins/module_utils/network/vyos/facts/nat/__init__.py b/plugins/module_utils/network/vyos/facts/nat/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/nat/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/nat/nat.py b/plugins/module_utils/network/vyos/facts/nat/nat.py new file mode 100644 index 00000000..73d6238f --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/nat/nat.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# 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 + +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 'set nat'") + + def populate_facts(self, connection, ansible_facts, data=None): + facts = {} + 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 k == "range" and isinstance(v, list): + existing = merged[name].setdefault(k, []) + existing.extend(v) + if v and isinstance(v[0], dict): + merged[name][k] = self._merge_range_list(existing) + else: + merged[name][k] = list(dict.fromkeys(existing)) + elif isinstance(v, list): + 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 _merge_range_list(self, ranges): + """Merge external pool range entries by value, preserving seq.""" + merged = {} + for entry in ranges: + if isinstance(entry, dict): + key = entry.get("value") or entry.get("address", "") + if not key: + continue + if key not in merged: + merged[key] = {"value": key} + if entry.get("seq"): + merged[key]["seq"] = entry["seq"] + else: + if entry not in merged: + merged[entry] = {"value": entry} + 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 not in nat: + continue + rules = nat[section].get("rule") + if isinstance(rules, list): + nat[section]["rule"] = self._merge_rule_list(rules) + 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 and isinstance(pool[ptype], list): + 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 and isinstance(pools, list): + rule["translation"]["pool"] = self._merge_rule_list(pools) + rule["translation"]["pool"].sort(key=lambda x: x.get("id", 0)) + + self._cast_ports(objs) + return objs + + def _cast_ports(self, obj): + """Recursively cast known integer port/seq fields to str.""" + if isinstance(obj, dict): + for k, v in obj.items(): + if k in ("port", "seq") and isinstance(v, int): + obj[k] = str(v) + else: + self._cast_ports(v) + elif isinstance(obj, list): + for item in obj: + self._cast_ports(item) diff --git a/plugins/module_utils/network/vyos/rm_templates/nat.py b/plugins/module_utils/network/vyos/rm_templates/nat.py new file mode 100644 index 00000000..94af8696 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/nat.py @@ -0,0 +1,1186 @@ +# -*- coding: utf-8 -*- +# 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 + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_nat_rule_addr_sub(config_data): + """Generate address/fqdn/prefix/port/group commands for destination or source sub-dict.""" + nat = config_data["nat"] + type_ = config_data["type"] + rid = config_data["id"] + atype = config_data["atype"] + sub = config_data["sub"] + + base = f"{nat} {type_} rule {rid} {atype}" + commands = [] + + for field in ("address", "fqdn", "prefix", "port"): + if sub.get(field) is not None: + commands.append(f"{base} {field} {sub[field]}") + + for gtype in ("address_group", "domain_group", "mac_group", "network_group", "port_group"): + if sub.get(gtype) is not None: + commands.append(f"{base} group {gtype.replace('_', '-')} {sub[gtype]}") + + return commands + + +def _tmplt_nat_rule_translation(config_data): + """Generate translation commands.""" + nat = config_data["nat"] + type_ = config_data["type"] + rid = config_data["id"] + trans = config_data["translation"] + + base = f"{nat} {type_} rule {rid} translation" + commands = [] + + if trans.get("address") is not None: + commands.append(f"{base} address {trans['address']}") + + if trans.get("port") is not None: + commands.append(f"{base} port {trans['port']}") + + if trans.get("redirect_port") is not None: + commands.append(f"{base} redirect port {trans['redirect_port']}") + + if trans.get("address_mapping") is not None: + commands.append(f"{base} options address-mapping {trans['address_mapping']}") + + if trans.get("port_mapping") is not None: + commands.append(f"{base} options port-mapping {trans['port_mapping']}") + + return commands + + +def _tmplt_nat64_translation_pool(config_data): + """Generate all nat64 translation pool commands from a single call.""" + nat = config_data["nat"] + type_ = config_data["type"] + rid = config_data["id"] + pool_id = config_data["pool_id"] + pool = config_data["pool"] + + base = f"{nat} {type_} rule {rid} translation pool {pool_id}" + commands = [] + + if pool.get("address") is not None: + commands.append(f"{base} address {pool['address']}") + if pool.get("description") is not None: + commands.append(f"{base} description '{pool['description']}'") + if pool.get("port") is not None: + commands.append(f"{base} port {pool['port']}") + if pool.get("protocol") is not None: + commands.append(f"{base} protocol {pool['protocol']}") + if pool.get("disable"): + commands.append(f"{base} disable") + + return commands + + +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) + + # fmt: off + PARSERS = [ + + # ------------------------- + # CGNAT + # ------------------------- + { + "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<name>\S+) + \s+range + \s+(?P<range>\S+)(?:\s+seq\s+(?P<seq>\d+))? + $""", + re.VERBOSE, + ), + "setval": "nat cgnat pool external {{ name }} range {{ range }}{% if seq is defined and seq %} seq {{ seq }}{% endif %}", + "result": { + "nat": { + "cgnat": { + "pool": { + "external": [ + { + "name": "{{ name }}", + "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<name>\S+) + \s+external-port-range + \s+(?P<range>\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<name>\S+) + \s+per-user-limit + \s+port + \s+(?P<limit>\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<name>\S+) + \s+range + \s+(?P<range>\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<id>\d+) + \s+source + \s+pool + \s+(?P<pool>\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<id>\d+) + \s+translation + \s+pool + \s+(?P<pool>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat rule {{ id }} translation pool {{ pool }}", + "result": { + "nat": { + "cgnat": { + "rule": [ + { + "id": "{{ id }}", + "translation": {"pool": "{{ pool }}"}, + }, + ], + }, + }, + }, + }, + + # ------------------------- + # GENERIC NAT + # ------------------------- + { + "name": "nat_type_description", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+description + \s+(?P<description>.+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} description '{{ description }}'", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "description": "{{ description }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_protocol", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+protocol + \s+(?P<protocol>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} protocol {{ protocol }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "protocol": "{{ protocol }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_disable", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\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+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\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+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+log + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} log", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "log": True, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_address", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+address + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"address": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_prefix", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+prefix + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"prefix": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_fqdn", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+fqdn + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"fqdn": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_port", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"port": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_address_group", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+group + \s+(?P<gtype>address-group|domain-group|mac-group|network-group|port-group) + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": { + "{{ gtype | replace('-', '_') }}": "{{ value }}", + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_address", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+address + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": {"address": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_port", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": {"port": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_options", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+options + \s+(?P<opt>address-mapping|port-mapping) + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "{{ opt | replace('-', '_') }}": "{{ value }}", + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_redirect", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+redirect + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "redirect_port": "{{ value }}", + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_inbound_interface_name", + "getval": re.compile( + r""" + ^set + \s+nat + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+name + \s+(?P<value>\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+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+group + \s+(?P<value>\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<id>\S+) + \s+inbound-interface + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat static rule {{ id }} inbound-interface {{ value }}", + "result": { + "nat": { + "static": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": "{{ value }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat6x_inbound_interface", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+name + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} inbound-interface name {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": {"name": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_outbound_interface", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+outbound-interface + \s+name + \s+(?P<value>\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+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+outbound-interface + \s+group + \s+(?P<value>\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_packet_type", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+packet-type + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} packet-type {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "packet_type": "{{ value }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_lb_backend", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+load-balance + \s+backend + \s+(?P<ip>\S+) + \s+weight + \s+(?P<weight>\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 }}"}, + ], + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_lb_hash", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+load-balance + \s+hash + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance hash {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "load_balance": {"hash": ["{{ value }}"]}, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_match_mark", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+match + \s+mark + \s+(?P<mark>\d+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} match mark {{ mark }}", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "match": {"mark": "{{ mark }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_translation_pool", + "getval": re.compile(r"^$"), # never matches — setval only + "setval": _tmplt_nat64_translation_pool, + "result": {}, + }, + { + "name": "nat64_translation_pool_address", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+address + \s+(?P<value>\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<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+description + \s+(?P<value>.+) + $""", + 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<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\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<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+port + \s+(?P<value>\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<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+protocol + \s+(?P<value>\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/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py index e95e8dea..89f12773 100644 --- a/plugins/module_utils/network/vyos/utils/utils.py +++ b/plugins/module_utils/network/vyos/utils/utils.py @@ -310,6 +310,7 @@ def combine(a, b, recursive=False, list_merge="replace"): :raises ValueError: If either argument is not a dict, or if an unsupported ``list_merge`` mode is given. """ + if not isinstance(a, dict) or not isinstance(b, dict): raise ValueError("combine expects two dictionaries") diff --git a/plugins/modules/vyos_nat.py b/plugins/modules/vyos_nat.py new file mode 100644 index 00000000..7715cb76 --- /dev/null +++ b/plugins/modules/vyos_nat.py @@ -0,0 +1,934 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# 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: 6.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.4.3 and 1.5.0. +- 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: 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. + load_balance: + type: dict + description: Load balancing configuration for this NAT rule. + suboptions: + hash: + type: list + elements: str + description: Fields to hash on for load balancing. Mutually exclusive with I(translation.address). + choices: + - source-address + - destination-address + - source-port + - destination-port + - random + backend: + type: list + elements: dict + description: List of backends to load-balance across. Weights should sum to 100. + suboptions: + ip: + type: str + description: IP address of the backend translation target. + weight: + type: int + description: Relative weight (1-100) for this backend's share of load-balanced traffic. + 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. + load_balance: + type: dict + description: Load balancing configuration for this NAT rule. + suboptions: + hash: + type: list + elements: str + description: Fields to hash on for load balancing. Mutually exclusive with I(translation.address). + choices: + - source-address + - destination-address + - source-port + - destination-port + - random + backend: + type: list + elements: dict + description: List of backends to load-balance across. Weights should sum to 100. + suboptions: + ip: + type: str + description: IP address of the backend translation target. + weight: + type: int + description: Relative weight (1-100) for this backend's share of load-balanced traffic. + 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 | match 'nat'). + - The state I(parsed) reads the configuration from C(show configuration commands | match '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 state I(replaced) replaces only the provided configuration, while I(overridden) removes any + existing NAT configuration not specified in I(config). + 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: + - value: 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 replaced - replace specific NAT rules +- name: Replace destination NAT rule + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Replaced web server NAT" + protocol: tcp + destination: + address: 198.51.100.10 + port: "443" + translation: + address: 192.168.1.10 + port: "8443" + state: replaced + +# Using overridden - override entire NAT configuration +- name: Override entire NAT configuration + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Only rule after override" + protocol: tcp + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: overridden + +# Using deleted - delete all NAT configuration +- name: Delete all NAT configuration + vyos.vyos.vyos_nat: + state: deleted + +# Using deleted - delete specific NAT rules +- name: Delete specific NAT rules + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + source: + rule: + - id: 200 + nat64: + source: + rule: + - id: 10 + state: deleted + +# Using gathered +- name: Gather NAT configuration from device + vyos.vyos.vyos_nat: + state: gathered + +# Using rendered +- name: Render NAT configuration offline + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Rendered rule" + protocol: tcp + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: rendered + +# Using parsed +- name: Parse NAT configuration from file + vyos.vyos.vyos_nat: + running_config: "{{ lookup('file', './nat_config.cfg') }}" + state: parsed +""" +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted) + 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) or C(deleted) + type: list + sample: + - set nat destination rule 100 description 'Web server NAT' + - set nat destination rule 100 protocol tcp + - set nat destination rule 100 inbound-interface name eth2 + - set nat destination rule 100 destination address 198.51.100.10 + - set nat destination rule 100 translation address 192.168.1.10 + - delete nat source rule 200 +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 nat destination rule 100 description 'Web server NAT' + - set nat destination rule 100 protocol tcp + - set nat destination rule 100 inbound-interface name eth2 + - set nat destination rule 100 destination address 198.51.100.10 + - set nat destination rule 100 translation address 192.168.1.10 +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: dict + 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: dict + 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.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() diff --git a/plugins/modules/vyos_vrf.py b/plugins/modules/vyos_vrf.py index defe24bb..3f7ae10e 100644 --- a/plugins/modules/vyos_vrf.py +++ b/plugins/modules/vyos_vrf.py @@ -1,8 +1,6 @@ #!/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) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ The module file for vyos_vrf diff --git a/tests/integration/targets/vyos_nat/defaults/main.yaml b/tests/integration/targets/vyos_nat/defaults/main.yaml new file mode 100644 index 00000000..164afead --- /dev/null +++ b/tests/integration/targets/vyos_nat/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_nat/tasks/cli.yaml b/tests/integration/targets/vyos_nat/tasks/cli.yaml new file mode 100644 index 00000000..a71206c0 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tasks/cli.yaml @@ -0,0 +1,20 @@ +--- +- name: Collect all cli test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}\\.yaml$" + use_regex: true + delegate_to: localhost + register: test_cases + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=ansible.netcommon.network_cli) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.network_cli + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_nat/tasks/main.yaml b/tests/integration/targets/vyos_nat/tasks/main.yaml new file mode 100644 index 00000000..e6378581 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run CLI tests + ansible.builtin.include_tasks: cli.yaml + tags: + - network_cli diff --git a/tests/integration/targets/vyos_nat/tests/cli/_parsed.cfg b/tests/integration/targets/vyos_nat/tests/cli/_parsed.cfg new file mode 100644 index 00000000..68375366 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/_parsed.cfg @@ -0,0 +1,17 @@ +set nat destination rule 100 description 'Web server NAT' +set nat destination rule 100 destination address '198.51.100.10' +set nat destination rule 100 destination port '80' +set nat destination rule 100 inbound-interface name 'eth2' +set nat destination rule 100 log +set nat destination rule 100 protocol 'tcp' +set nat destination rule 100 translation address '192.168.1.10' +set nat destination rule 100 translation port '8080' +set nat source rule 200 description 'Outbound NAT' +set nat source rule 200 destination address '192.168.10.100' +set nat source rule 200 destination port '8083' +set nat source rule 200 disable +set nat source rule 200 exclude +set nat source rule 200 log +set nat source rule 200 protocol 'tcp' +set nat source rule 200 translation address 'masquerade' +set nat source rule 200 translation port '443' diff --git a/tests/integration/targets/vyos_nat/tests/cli/_populate.yaml b/tests/integration/targets/vyos_nat/tests/cli/_populate.yaml new file mode 100644 index 00000000..a3715be9 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/_populate.yaml @@ -0,0 +1,8 @@ +--- +- ansible.builtin.include_tasks: _remove_config.yaml + +- name: Setup NAT configuration + vyos.vyos.vyos_config: + lines: "{{ populate_config }}" + vars: + ansible_connection: ansible.netcommon.network_cli diff --git a/tests/integration/targets/vyos_nat/tests/cli/_remove_config.yaml b/tests/integration/targets/vyos_nat/tests/cli/_remove_config.yaml new file mode 100644 index 00000000..3618bf40 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/_remove_config.yaml @@ -0,0 +1,10 @@ +--- +- name: Remove NAT configuration + vyos.vyos.vyos_config: + lines: + - delete nat + - delete nat64 + - delete nat66 + ignore_errors: true + vars: + ansible_connection: ansible.netcommon.network_cli diff --git a/tests/integration/targets/vyos_nat/tests/cli/deleted.yaml b/tests/integration/targets/vyos_nat/tests/cli/deleted.yaml new file mode 100644 index 00000000..c08d6b1c --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/deleted.yaml @@ -0,0 +1,34 @@ +--- +- debug: + msg: START vyos_nat deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Delete all NAT configuration + register: result + vyos.vyos.vyos_nat: &id001 + config: + state: deleted + + - assert: + that: + - result.changed == true + - result.commands|symmetric_difference(deleted.commands) == [] + + - name: Assert that the after dicts were correctly generated + assert: + that: + - result.after|symmetric_difference(deleted.after) == [] + + - name: Delete the existing configuration (IDEMPOTENT) + register: result + vyos.vyos.vyos_nat: *id001 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/deleted_full_match.yaml b/tests/integration/targets/vyos_nat/tests/cli/deleted_full_match.yaml new file mode 100644 index 00000000..43a8124f --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/deleted_full_match.yaml @@ -0,0 +1,70 @@ +--- +- debug: + msg: START vyos_nat deleted full-match integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Delete destination rule 100 with full matching config + register: result + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: Web server NAT + protocol: tcp + log: true + inbound_interface: + name: eth2 + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: deleted + + - assert: + that: + - result.changed == true + - result.commands == ["delete nat destination rule 100"] + + - vyos.vyos.vyos_facts: + gather_network_resources: nat + + - name: Assert rule 100 is actually gone from the device + assert: + that: + - ansible_facts['network_resources']['nat'].nat.destination is not defined or + ansible_facts['network_resources']['nat'].nat.destination.rule | default([]) | selectattr('id', 'equalto', 100) | list | length == 0 + + - name: Delete the same configuration again (IDEMPOTENT) + register: result + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: Web server NAT + protocol: tcp + log: true + inbound_interface: + name: eth2 + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: deleted + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/empty_config.yaml b/tests/integration/targets/vyos_nat/tests/cli/empty_config.yaml new file mode 100644 index 00000000..39b57fee --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/empty_config.yaml @@ -0,0 +1,60 @@ +--- +- debug: + msg: START vyos_nat empty_config integration tests on connection={{ ansible_connection }} + +- name: Merged with empty config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_nat: + config: + state: merged + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state merged' + +- name: Replaced with empty config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_nat: + config: + state: replaced + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state replaced' + +- name: Overridden with empty config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_nat: + config: + state: overridden + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state overridden' + +- name: Parsed with empty running_config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_nat: + running_config: + state: parsed + +- assert: + that: + - result.msg == 'value of running_config parameter must not be empty for state parsed' + +- name: Rendered with empty config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_nat: + config: + state: rendered + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state rendered' + +- include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/gathered.yaml b/tests/integration/targets/vyos_nat/tests/cli/gathered.yaml new file mode 100644 index 00000000..a91410b8 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/gathered.yaml @@ -0,0 +1,22 @@ +--- +- debug: + msg: START vyos_nat gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Gather NAT config from the device + register: result + vyos.vyos.vyos_nat: + state: gathered + + - vyos.vyos.vyos_facts: + gather_network_resources: nat + + - assert: + that: + - result.changed == false + - result.gathered|symmetric_difference(ansible_facts['network_resources']['nat']) == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/merged.yaml b/tests/integration/targets/vyos_nat/tests/cli/merged.yaml new file mode 100644 index 00000000..73184fb6 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/merged.yaml @@ -0,0 +1,110 @@ +--- +- debug: + msg: START vyos_nat merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Merge new NAT rule with existing configuration + register: result + vyos.vyos.vyos_nat: &id001 + config: + nat: + destination: + rule: + - id: 101 + description: New DNAT rule + protocol: tcp + destination: + address: 198.51.100.11 + port: "443" + translation: + address: 192.168.1.11 + port: "8443" + state: merged + + - vyos.vyos.vyos_facts: + gather_network_resources: nat + + - assert: + that: + - result.changed == true + - result.commands|symmetric_difference(merged.commands) == [] + - result.after|symmetric_difference(ansible_facts['network_resources']['nat']) == [] + + - name: Assert that before dicts were correctly generated + assert: + that: + - merged['before'] | symmetric_difference(result['before']) | length == 0 + + - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) + register: result + vyos.vyos.vyos_nat: *id001 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + + - name: Add static rule with plain inbound_interface string + register: result + vyos.vyos.vyos_nat: &id002 + config: + nat: + static: + rule: + - id: 300 + description: Static mapping + inbound_interface: eth2 + destination: + address: 192.168.100.20 + translation: + address: 192.168.1.20 + state: merged + + - assert: + that: + - result.changed == true + - "'set nat static rule 300 inbound-interface eth2' in result.commands" + + - name: Add static rule again (IDEMPOTENT) + register: result + vyos.vyos.vyos_nat: *id002 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + + - name: Change static rule inbound_interface + register: result + vyos.vyos.vyos_nat: &id003 + config: + nat: + static: + rule: + - id: 300 + description: Static mapping + inbound_interface: eth3 + destination: + address: 192.168.100.20 + translation: + address: 192.168.1.20 + state: merged + + - assert: + that: + - result.changed == true + - result.commands == ["set nat static rule 300 inbound-interface eth3"] + + - name: Change static rule inbound_interface again (IDEMPOTENT) + register: result + vyos.vyos.vyos_nat: *id003 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/merged_load_balance.yaml b/tests/integration/targets/vyos_nat/tests/cli/merged_load_balance.yaml new file mode 100644 index 00000000..04ee5816 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/merged_load_balance.yaml @@ -0,0 +1,107 @@ +--- +- debug: + msg: START vyos_nat load balance integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Add destination rule with load-balance hash and backends + register: result + vyos.vyos.vyos_nat: &id001 + config: + nat: + destination: + rule: + - id: 105 + protocol: tcp + destination: + port: "80" + inbound_interface: + name: eth0 + load_balance: + hash: + - source-address + - destination-address + backend: + - ip: 10.10.10.1 + weight: 60 + - ip: 10.10.10.2 + weight: 40 + state: merged + + - assert: + that: + - result.changed == true + - "'set nat destination rule 105 load-balance hash source-address' in result.commands" + - "'set nat destination rule 105 load-balance hash destination-address' in result.commands" + - "'set nat destination rule 105 load-balance backend 10.10.10.1 weight 60' in result.commands" + - "'set nat destination rule 105 load-balance backend 10.10.10.2 weight 40' in result.commands" + + - name: Re-apply same load-balance config (IDEMPOTENT) + register: result + vyos.vyos.vyos_nat: *id001 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + + - name: Change backend weights only (in-place overwrite check) + register: result + vyos.vyos.vyos_nat: &id002 + config: + nat: + destination: + rule: + - id: 105 + protocol: tcp + destination: + port: "80" + inbound_interface: + name: eth0 + load_balance: + hash: + - source-address + - destination-address + backend: + - ip: 10.10.10.1 + weight: 70 + - ip: 10.10.10.2 + weight: 30 + state: merged + + - assert: + that: + - result.changed == true + + - vyos.vyos.vyos_facts: + gather_network_resources: nat + + - name: Extract rule 105 from gathered facts + set_fact: + rule_105: "{{ ansible_facts['network_resources']['nat'].nat.destination.rule | selectattr('id', 'equalto', 105) | first }}" + + - name: Extract backend weights from rule 105 + set_fact: + backend_105: "{{ rule_105.load_balance.backend }}" + weight_101: "{{ (rule_105.load_balance.backend | selectattr('ip', 'equalto', '10.10.10.1') | first).weight }}" + weight_102: "{{ (rule_105.load_balance.backend | selectattr('ip', 'equalto', '10.10.10.2') | first).weight }}" + + - name: Assert weights updated and no duplicate/stale backend entries + assert: + that: + - backend_105 | length == 2 + - weight_101 == 70 + - weight_102 == 30 + + - name: Re-apply changed weights again (IDEMPOTENT) + register: result + vyos.vyos.vyos_nat: *id002 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/overridden.yaml b/tests/integration/targets/vyos_nat/tests/cli/overridden.yaml new file mode 100644 index 00000000..fbb87d72 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/overridden.yaml @@ -0,0 +1,45 @@ +--- +- debug: + msg: START vyos_nat overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Override NAT configuration + register: result + vyos.vyos.vyos_nat: &id001 + config: + nat: + destination: + rule: + - id: 100 + description: Overridden DNAT rule + protocol: tcp + destination: + address: 198.51.100.20 + port: "443" + translation: + address: 192.168.1.20 + port: "8443" + state: overridden + + - vyos.vyos.vyos_facts: + gather_network_resources: nat + + - assert: + that: + - result.changed == true + - result.commands|symmetric_difference(overridden.commands) == [] + - result.after|symmetric_difference(ansible_facts['network_resources']['nat']) == [] + + - name: Override the existing configuration (IDEMPOTENT) + register: result + vyos.vyos.vyos_nat: *id001 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/parsed.yaml b/tests/integration/targets/vyos_nat/tests/cli/parsed.yaml new file mode 100644 index 00000000..13a54f42 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/parsed.yaml @@ -0,0 +1,16 @@ +--- +- debug: + msg: START vyos_nat parsed integration tests on connection={{ ansible_connection }} + +- name: Parse NAT configuration from file + register: result + vyos.vyos.vyos_nat: + running_config: "{{ lookup('file', '_parsed.cfg') }}" + state: parsed + +- assert: + that: + - result.changed == false + - result.parsed|symmetric_difference(merged.before) == [] + +- include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/rendered.yaml b/tests/integration/targets/vyos_nat/tests/cli/rendered.yaml new file mode 100644 index 00000000..9c7a8ccb --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/rendered.yaml @@ -0,0 +1,32 @@ +--- +- debug: + msg: START vyos_nat rendered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Render NAT configuration without applying to device + register: result + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: Rendered rule + protocol: tcp + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: rendered + + - assert: + that: + - result.changed == false + - result.rendered|symmetric_difference(rendered.commands) == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/replaced.yaml b/tests/integration/targets/vyos_nat/tests/cli/replaced.yaml new file mode 100644 index 00000000..a327c623 --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/replaced.yaml @@ -0,0 +1,45 @@ +--- +- debug: + msg: START vyos_nat replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Replace NAT destination rule 100 + register: result + vyos.vyos.vyos_nat: &id001 + config: + nat: + destination: + rule: + - id: 100 + description: Replaced DNAT rule + protocol: udp + destination: + address: 198.51.100.10 + port: "53" + translation: + address: 192.168.1.53 + port: "53" + state: replaced + + - vyos.vyos.vyos_facts: + gather_network_resources: nat + + - assert: + that: + - result.changed == true + - result.commands|symmetric_difference(replaced.commands) == [] + - result.after|symmetric_difference(ansible_facts['network_resources']['nat']) == [] + + - name: Replace the provided configuration (IDEMPOTENT) + register: result + vyos.vyos.vyos_nat: *id001 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/replaced_cgnat_pool.yaml b/tests/integration/targets/vyos_nat/tests/cli/replaced_cgnat_pool.yaml new file mode 100644 index 00000000..77fc295b --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/replaced_cgnat_pool.yaml @@ -0,0 +1,77 @@ +--- +- debug: + msg: START vyos_nat replaced cgnat pool integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Establish baseline CGNAT external pool + 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: + - value: 203.0.113.0/24 + - value: 203.1.113.1-203.1.113.60 + seq: "10" + 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 + + - name: Replace CGNAT pool changing only external_port_range + register: result + vyos.vyos.vyos_nat: + config: + nat: + cgnat: + log_allocation: true + pool: + external: + - name: ext-pool-1 + external_port_range: "30000-40000" + per_user_limit: + port: "200" + range: + - value: 203.0.113.0/24 + - value: 203.1.113.1-203.1.113.60 + seq: "10" + 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: replaced + + - vyos.vyos.vyos_facts: + gather_network_resources: nat + + - name: Assert all pool fields survived the replace, not just the changed one + assert: + that: + - result.changed == true + - ansible_facts['network_resources']['nat'].nat.cgnat.pool.external[0].external_port_range == "30000-40000" + - ansible_facts['network_resources']['nat'].nat.cgnat.pool.external[0].per_user_limit.port == "200" + - ansible_facts['network_resources']['nat'].nat.cgnat.pool.external[0].range | length == 2 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/tests/cli/rtt.yaml b/tests/integration/targets/vyos_nat/tests/cli/rtt.yaml new file mode 100644 index 00000000..8ec9397e --- /dev/null +++ b/tests/integration/targets/vyos_nat/tests/cli/rtt.yaml @@ -0,0 +1,70 @@ +--- +- debug: + msg: START vyos_nat round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Apply additional NAT rule to establish base config + register: baseconfig + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: Web server NAT + protocol: tcp + log: true + inbound_interface: + name: eth2 + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + - id: 101 + description: RTT base rule + protocol: tcp + destination: + address: 198.51.100.11 + port: "443" + translation: + address: 192.168.1.11 + port: "8443" + state: merged + + - vyos.vyos.vyos_facts: + gather_network_resources: nat + + - name: Modify rule 101 description + register: result + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 101 + description: RTT modified rule + protocol: tcp + destination: + address: 198.51.100.11 + port: "443" + translation: + address: 192.168.1.11 + port: "8443" + state: merged + + - name: Revert back to base config using facts round trip + register: revert + vyos.vyos.vyos_nat: + config: "{{ ansible_facts['network_resources']['nat'] }}" + state: overridden + + - name: Assert that config was reverted + assert: + that: baseconfig.after == revert.after + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_nat/vars/main.yaml b/tests/integration/targets/vyos_nat/vars/main.yaml new file mode 100644 index 00000000..0bae2c03 --- /dev/null +++ b/tests/integration/targets/vyos_nat/vars/main.yaml @@ -0,0 +1,172 @@ +--- +populate_config: + - set nat destination rule 100 description 'Web server NAT' + - set nat destination rule 100 destination address '198.51.100.10' + - set nat destination rule 100 destination port '80' + - set nat destination rule 100 inbound-interface name 'eth2' + - set nat destination rule 100 log + - set nat destination rule 100 protocol 'tcp' + - set nat destination rule 100 translation address '192.168.1.10' + - set nat destination rule 100 translation port '8080' + - set nat source rule 200 description 'Outbound NAT' + - set nat source rule 200 destination address '192.168.10.100' + - set nat source rule 200 destination port '8083' + - set nat source rule 200 disable + - set nat source rule 200 exclude + - set nat source rule 200 log + - set nat source rule 200 protocol 'tcp' + - set nat source rule 200 translation address 'masquerade' + - set nat source rule 200 translation port '443' + +merged: + before: + nat: + destination: + rule: + - id: 100 + description: Web server NAT + protocol: tcp + log: true + inbound_interface: + name: eth2 + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + source: + rule: + - id: 200 + description: Outbound NAT + protocol: tcp + log: true + disable: true + exclude: true + destination: + address: 192.168.10.100 + port: "8083" + translation: + address: masquerade + port: "443" + commands: + - set nat destination rule 101 description 'New DNAT rule' + - set nat destination rule 101 protocol tcp + - set nat destination rule 101 destination address 198.51.100.11 + - set nat destination rule 101 destination port 443 + - set nat destination rule 101 translation address 192.168.1.11 + - set nat destination rule 101 translation port 8443 + after: + nat: + destination: + rule: + - id: 100 + description: Web server NAT + protocol: tcp + log: true + inbound_interface: + name: eth2 + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + - id: 101 + description: New DNAT rule + protocol: tcp + destination: + address: 198.51.100.11 + port: "443" + translation: + address: 192.168.1.11 + port: "8443" + source: + rule: + - id: 200 + description: Outbound NAT + protocol: tcp + log: true + disable: true + exclude: true + destination: + address: 192.168.10.100 + port: "8083" + translation: + address: masquerade + port: "443" + +replaced: + commands: + - delete nat destination rule 100 + - set nat destination rule 100 description 'Replaced DNAT rule' + - set nat destination rule 100 protocol udp + - set nat destination rule 100 destination address 198.51.100.10 + - set nat destination rule 100 destination port 53 + - set nat destination rule 100 translation address 192.168.1.53 + - set nat destination rule 100 translation port 53 + after: + nat: + destination: + rule: + - id: 100 + description: Replaced DNAT rule + protocol: udp + destination: + address: 198.51.100.10 + port: "53" + translation: + address: 192.168.1.53 + port: "53" + source: + rule: + - id: 200 + description: Outbound NAT + protocol: tcp + log: true + disable: true + exclude: true + destination: + address: 192.168.10.100 + port: "8083" + translation: + address: masquerade + port: "443" + +overridden: + commands: + - delete nat source + - delete nat destination rule 100 + - set nat destination rule 100 description 'Overridden DNAT rule' + - set nat destination rule 100 protocol tcp + - set nat destination rule 100 destination address 198.51.100.20 + - set nat destination rule 100 destination port 443 + - set nat destination rule 100 translation address 192.168.1.20 + - set nat destination rule 100 translation port 8443 + after: + nat: + destination: + rule: + - id: 100 + description: Overridden DNAT rule + protocol: tcp + destination: + address: 198.51.100.20 + port: "443" + translation: + address: 192.168.1.20 + port: "8443" + +deleted: + commands: + - delete nat + after: {} + +rendered: + commands: + - set nat destination rule 100 description 'Rendered rule' + - set nat destination rule 100 protocol tcp + - set nat destination rule 100 destination address 198.51.100.10 + - set nat destination rule 100 destination port 80 + - set nat destination rule 100 translation address 192.168.1.10 + - set nat destination rule 100 translation port 8080 diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 255b0a87..ee3d2aac 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs plugins/action/lldp_interface.py action-plugin-docs plugins/action/lldp_interfaces.py action-plugin-docs plugins/action/logging_global.py action-plugin-docs +plugins/action/nat.py action-plugin-docs plugins/action/ntp_global.py action-plugin-docs plugins/action/ospf_interfaces.py action-plugin-docs plugins/action/ospfv2.py action-plugin-docs diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index 255b0a87..ee3d2aac 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs plugins/action/lldp_interface.py action-plugin-docs plugins/action/lldp_interfaces.py action-plugin-docs plugins/action/logging_global.py action-plugin-docs +plugins/action/nat.py action-plugin-docs plugins/action/ntp_global.py action-plugin-docs plugins/action/ospf_interfaces.py action-plugin-docs plugins/action/ospfv2.py action-plugin-docs diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt index 255b0a87..ee3d2aac 100644 --- a/tests/sanity/ignore-2.17.txt +++ b/tests/sanity/ignore-2.17.txt @@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs plugins/action/lldp_interface.py action-plugin-docs plugins/action/lldp_interfaces.py action-plugin-docs plugins/action/logging_global.py action-plugin-docs +plugins/action/nat.py action-plugin-docs plugins/action/ntp_global.py action-plugin-docs plugins/action/ospf_interfaces.py action-plugin-docs plugins/action/ospfv2.py action-plugin-docs diff --git a/tests/sanity/ignore-2.18.txt b/tests/sanity/ignore-2.18.txt index 255b0a87..ee3d2aac 100644 --- a/tests/sanity/ignore-2.18.txt +++ b/tests/sanity/ignore-2.18.txt @@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs plugins/action/lldp_interface.py action-plugin-docs plugins/action/lldp_interfaces.py action-plugin-docs plugins/action/logging_global.py action-plugin-docs +plugins/action/nat.py action-plugin-docs plugins/action/ntp_global.py action-plugin-docs plugins/action/ospf_interfaces.py action-plugin-docs plugins/action/ospfv2.py action-plugin-docs diff --git a/tests/sanity/ignore-2.19.txt b/tests/sanity/ignore-2.19.txt index 255b0a87..ee3d2aac 100644 --- a/tests/sanity/ignore-2.19.txt +++ b/tests/sanity/ignore-2.19.txt @@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs plugins/action/lldp_interface.py action-plugin-docs plugins/action/lldp_interfaces.py action-plugin-docs plugins/action/logging_global.py action-plugin-docs +plugins/action/nat.py action-plugin-docs plugins/action/ntp_global.py action-plugin-docs plugins/action/ospf_interfaces.py action-plugin-docs plugins/action/ospfv2.py action-plugin-docs diff --git a/tests/sanity/ignore-2.20.txt b/tests/sanity/ignore-2.20.txt index 255b0a87..a96f7e91 100644 --- a/tests/sanity/ignore-2.20.txt +++ b/tests/sanity/ignore-2.20.txt @@ -20,6 +20,7 @@ plugins/action/lldp.py action-plugin-docs plugins/action/lldp_global.py action-plugin-docs plugins/action/lldp_interface.py action-plugin-docs plugins/action/lldp_interfaces.py action-plugin-docs +plugins/action/nat.py action-plugin-docs plugins/action/logging_global.py action-plugin-docs plugins/action/ntp_global.py action-plugin-docs plugins/action/ospf_interfaces.py action-plugin-docs diff --git a/tests/sanity/ignore-2.21.txt b/tests/sanity/ignore-2.21.txt index 255b0a87..ee3d2aac 100644 --- a/tests/sanity/ignore-2.21.txt +++ b/tests/sanity/ignore-2.21.txt @@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs plugins/action/lldp_interface.py action-plugin-docs plugins/action/lldp_interfaces.py action-plugin-docs plugins/action/logging_global.py action-plugin-docs +plugins/action/nat.py action-plugin-docs plugins/action/ntp_global.py action-plugin-docs plugins/action/ospf_interfaces.py action-plugin-docs plugins/action/ospfv2.py action-plugin-docs diff --git a/tests/sanity/ignore-2.22.txt b/tests/sanity/ignore-2.22.txt index 255b0a87..ee3d2aac 100644 --- a/tests/sanity/ignore-2.22.txt +++ b/tests/sanity/ignore-2.22.txt @@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs plugins/action/lldp_interface.py action-plugin-docs plugins/action/lldp_interfaces.py action-plugin-docs plugins/action/logging_global.py action-plugin-docs +plugins/action/nat.py action-plugin-docs plugins/action/ntp_global.py action-plugin-docs plugins/action/ospf_interfaces.py action-plugin-docs plugins/action/ospfv2.py action-plugin-docs diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg new file mode 100644 index 00000000..505f5e57 --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg @@ -0,0 +1,61 @@ +set nat cgnat log-allocation +set nat cgnat pool external ext-pool-1 external-port-range '10000-20000' +set nat cgnat pool external ext-pool-1 per-user-limit port '200' +set nat cgnat pool external ext-pool-1 range 203.0.113.0/24 +set nat cgnat pool external ext-pool-1 range 203.1.113.1-203.1.113.60 seq '10' +set nat cgnat pool internal int-pool-1 range '10.0.0.0/24' +set nat cgnat pool internal int-pool-1 range '10.1.0.0/24' +set nat cgnat rule 1 source pool 'int-pool-1' +set nat cgnat rule 1 translation pool 'ext-pool-1' +set nat destination rule 100 description 'Web server NAT' +set nat destination rule 100 destination address '198.51.100.10' +set nat destination rule 100 destination port '80' +set nat destination rule 100 inbound-interface name 'eth2' +set nat destination rule 100 log +set nat destination rule 100 protocol 'tcp' +set nat destination rule 100 translation address '192.168.1.10' +set nat destination rule 100 translation options address-mapping 'persistent' +set nat destination rule 100 translation options port-mapping 'random' +set nat destination rule 100 translation port '8080' +set nat destination rule 105 protocol tcp +set nat destination rule 105 destination port '80' +set nat destination rule 105 inbound-interface name 'eth0' +set nat destination rule 105 load-balance hash 'source-address' +set nat destination rule 105 load-balance hash 'destination-address' +set nat destination rule 105 load-balance backend 10.10.10.1 weight '60' +set nat destination rule 105 load-balance backend 10.10.10.2 weight '40' +set nat source rule 200 description 'Outbound NAT' +set nat source rule 200 destination address '192.168.10.100' +set nat source rule 200 destination port '8083' +set nat source rule 200 disable +set nat source rule 200 exclude +set nat source rule 200 log +set nat source rule 200 protocol 'tcp' +set nat source rule 200 translation address 'masquerade' +set nat source rule 200 translation port '443' +set nat static rule 300 description 'Static mapping' +set nat static rule 300 destination address '192.168.100.20' +set nat static rule 300 inbound-interface 'eth2' +set nat static rule 300 log +set nat static rule 300 translation address '192.168.1.20' +set nat64 source rule 10 description 'NAT64 example' +set nat64 source rule 10 disable +set nat64 source rule 10 match mark '100' +set nat64 source rule 10 source prefix '2001:db8::/96' +set nat64 source rule 10 translation pool 1 address '192.168.100.10' +set nat64 source rule 10 translation pool 1 description 'NAT64 translation pool' +set nat64 source rule 10 translation pool 1 disable +set nat64 source rule 10 translation pool 1 port '1-65535' +set nat64 source rule 10 translation pool 1 protocol udp +set nat66 destination rule 20 description 'NAT66 DNAT' +set nat66 destination rule 20 destination address '2001:db8::1' +set nat66 destination rule 20 inbound-interface name 'eth1' +set nat66 destination rule 20 protocol 'tcp' +set nat66 destination rule 20 translation address '2001:db8:1::10' +set nat66 destination rule 20 translation port '8443' +set nat66 source rule 30 description 'NAT66 SNAT' +set nat66 source rule 30 destination prefix '2001:db8::/96' +set nat66 source rule 30 outbound-interface name 'eth2' +set nat66 source rule 30 protocol 'tcp' +set nat66 source rule 30 source prefix '2001:db8:2::/64' +set nat66 source rule 30 translation address 'masquerade' diff --git a/tests/unit/modules/network/vyos/test_vyos_nat.py b/tests/unit/modules/network/vyos/test_vyos_nat.py new file mode 100644 index 00000000..4ef76305 --- /dev/null +++ b/tests/unit/modules/network/vyos/test_vyos_nat.py @@ -0,0 +1,839 @@ +# (c) 2024 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from unittest.mock import patch + +from ansible_collections.vyos.vyos.plugins.modules import vyos_nat +from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args + +from .vyos_module import TestVyosModule, load_fixture + + +class TestVyosNatModule(TestVyosModule): + module = vyos_nat + + def setUp(self): + super(TestVyosNatModule, self).setUp() + + self.mock_get_resource_connection_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base.get_resource_connection", + ) + self.get_resource_connection_config = self.mock_get_resource_connection_config.start() + + self.mock_get_resource_connection_facts = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection", + ) + self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start() + + self.mock_execute_show_command = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.nat.nat.NatFacts.get_config", + ) + self.execute_show_command = self.mock_execute_show_command.start() + self.maxDiff = None + + def tearDown(self): + super(TestVyosNatModule, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_execute_show_command.stop() + + def load_fixtures(self, commands=None, filename=None): + if filename is None: + filename = "vyos_nat_config.cfg" + + def load_from_file(*args, **kwargs): + return load_fixture(filename) + + self.execute_show_command.side_effect = load_from_file + + # ------------------------------------------------------------------------- + # merged + # ------------------------------------------------------------------------- + + def test_vyos_nat_merged_idempotent(self): + set_module_args( + dict( + config=dict( + nat=dict( + cgnat=dict( + log_allocation=True, + pool=dict( + external=[ + dict( + name="ext-pool-1", + external_port_range="10000-20000", + per_user_limit=dict(port="200"), + range=[ + dict(value="203.0.113.0/24"), + dict(value="203.1.113.1-203.1.113.60", seq="10"), + ], + ), + ], + internal=[ + dict( + name="int-pool-1", + range=["10.0.0.0/24", "10.1.0.0/24"], + ), + ], + ), + rule=[ + dict( + id=1, + source=dict(pool="int-pool-1"), + translation=dict(pool="ext-pool-1"), + ), + ], + ), + destination=dict( + rule=[ + dict( + id=100, + description="Web server NAT", + protocol="tcp", + log=True, + inbound_interface=dict(name="eth2"), + destination=dict(address="198.51.100.10", port="80"), + translation=dict( + address="192.168.1.10", + port="8080", + address_mapping="persistent", + port_mapping="random", + ), + ), + dict( + id=105, + protocol="tcp", + destination=dict(port="80"), + inbound_interface=dict(name="eth0"), + load_balance=dict( + hash=["source-address", "destination-address"], + backend=[ + dict(ip="10.10.10.1", weight=60), + dict(ip="10.10.10.2", weight=40), + ], + ), + ), + ], + ), + source=dict( + rule=[ + dict( + id=200, + description="Outbound NAT", + protocol="tcp", + log=True, + exclude=True, + disable=True, + destination=dict(address="192.168.10.100", port="8083"), + translation=dict(address="masquerade", port="443"), + ), + ], + ), + static=dict( + rule=[ + dict( + id=300, + description="Static mapping", + inbound_interface="eth2", + destination=dict(address="192.168.100.20"), + translation=dict(address="192.168.1.20"), + log=True, + ), + ], + ), + ), + nat64=dict( + source=dict( + rule=[ + dict( + id=10, + description="NAT64 example", + disable=True, + match=dict(mark=100), + source=dict(prefix="2001:db8::/96"), + translation=dict( + pool=[ + dict( + id=1, + address="192.168.100.10", + description="NAT64 translation pool", + disable=True, + port="1-65535", + protocol="udp", + ), + ], + ), + ), + ], + ), + ), + nat66=dict( + destination=dict( + rule=[ + dict( + id=20, + description="NAT66 DNAT", + protocol="tcp", + inbound_interface=dict(name="eth1"), + destination=dict(address="2001:db8::1"), + translation=dict(address="2001:db8:1::10", port="8443"), + ), + ], + ), + source=dict( + rule=[ + dict( + id=30, + description="NAT66 SNAT", + protocol="tcp", + destination=dict(prefix="2001:db8::/96"), + outbound_interface=dict(name="eth2"), + source=dict(prefix="2001:db8:2::/64"), + translation=dict(address="masquerade"), + ), + ], + ), + ), + ), + state="merged", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_nat_merged_new_rule(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=101, + description="New DNAT rule", + protocol="tcp", + destination=dict(address="198.51.100.11", port="443"), + translation=dict(address="192.168.1.11", port="8443"), + ), + ], + ), + ), + ), + state="merged", + ), + ) + commands = [ + "set nat destination rule 101 description 'New DNAT rule'", + "set nat destination rule 101 protocol tcp", + "set nat destination rule 101 destination address 198.51.100.11", + "set nat destination rule 101 destination port 443", + "set nat destination rule 101 translation address 192.168.1.11", + "set nat destination rule 101 translation port 8443", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_merged_update_existing_rule(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=100, + description="Updated DNAT rule", + protocol="tcp", + inbound_interface=dict(name="eth2"), + destination=dict(address="198.51.100.10", port="80"), + translation=dict( + address="192.168.1.10", + port="8080", + address_mapping="persistent", + port_mapping="random", + ), + ), + ], + ), + ), + ), + state="merged", + ), + ) + commands = [ + "set nat destination rule 100 description 'Updated DNAT rule'", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_merged_cgnat_new_pool(self): + set_module_args( + dict( + config=dict( + nat=dict( + cgnat=dict( + pool=dict( + external=[ + dict( + name="ext-pool-2", + external_port_range="30000-40000", + range=[dict(value="203.0.114.0/24")], + ), + ], + ), + ), + ), + ), + state="merged", + ), + ) + commands = [ + "set nat cgnat pool external ext-pool-2 external-port-range 30000-40000", + "set nat cgnat pool external ext-pool-2 range 203.0.114.0/24", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_merged_nat66_new_rule(self): + set_module_args( + dict( + config=dict( + nat66=dict( + source=dict( + rule=[ + dict( + id=31, + description="New NAT66 SNAT", + protocol="udp", + outbound_interface=dict(name="eth3"), + source=dict(prefix="2001:db8:3::/64"), + translation=dict(address="masquerade"), + ), + ], + ), + ), + ), + state="merged", + ), + ) + commands = [ + "set nat66 source rule 31 description 'New NAT66 SNAT'", + "set nat66 source rule 31 protocol udp", + "set nat66 source rule 31 outbound-interface name eth3", + "set nat66 source rule 31 source prefix 2001:db8:3::/64", + "set nat66 source rule 31 translation address masquerade", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_merged_static_inbound_interface_change(self): + set_module_args( + dict( + config=dict( + nat=dict( + static=dict( + rule=[ + dict( + id=300, + description="Static mapping", + inbound_interface="eth3", + destination=dict(address="192.168.100.20"), + translation=dict(address="192.168.1.20"), + log=True, + ), + ], + ), + ), + ), + state="merged", + ), + ) + commands = ["set nat static rule 300 inbound-interface eth3"] + self.execute_module(changed=True, commands=commands) + + # ------------------------------------------------------------------------- + # deleted + # ------------------------------------------------------------------------- + + def test_vyos_nat_deleted_all(self): + set_module_args(dict(state="deleted")) + commands = [ + "delete nat", + "delete nat64", + "delete nat66", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_deleted_specific_rules(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict(rule=[dict(id=100)]), + source=dict(rule=[dict(id=200)]), + ), + ), + state="deleted", + ), + ) + commands = [ + "delete nat destination rule 100", + "delete nat source rule 200", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_deleted_cgnat_pool(self): + set_module_args( + dict( + config=dict( + nat=dict( + cgnat=dict( + pool=dict( + external=[dict(name="ext-pool-1")], + internal=[dict(name="int-pool-1")], + ), + ), + ), + ), + state="deleted", + ), + ) + commands = [ + "delete nat cgnat pool external ext-pool-1", + "delete nat cgnat pool internal int-pool-1", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_deleted_nat64_rule(self): + set_module_args( + dict( + config=dict( + nat64=dict( + source=dict(rule=[dict(id=10)]), + ), + ), + state="deleted", + ), + ) + commands = ["delete nat64 source rule 10"] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_deleted_nonexistent_rule(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict(rule=[dict(id=999)]), + ), + ), + state="deleted", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_nat_deleted_cgnat_rule_full_match(self): + set_module_args( + dict( + config=dict( + nat=dict( + cgnat=dict( + rule=[ + dict( + id=1, + source=dict(pool="int-pool-1"), + translation=dict(pool="ext-pool-1"), + ), + ], + ), + ), + ), + state="deleted", + ), + ) + commands = ["delete nat cgnat rule 1"] + self.execute_module(changed=True, commands=commands) + + # ------------------------------------------------------------------------- + # replaced + # ------------------------------------------------------------------------- + + def test_vyos_nat_replaced_idempotent(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=100, + description="Web server NAT", + protocol="tcp", + log=True, + inbound_interface=dict(name="eth2"), + destination=dict(address="198.51.100.10", port="80"), + translation=dict( + address="192.168.1.10", + port="8080", + address_mapping="persistent", + port_mapping="random", + ), + ), + ], + ), + ), + ), + state="replaced", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_nat_replaced_rule(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=100, + description="Replaced DNAT rule", + protocol="udp", + destination=dict(address="198.51.100.10", port="53"), + translation=dict(address="192.168.1.53", port="53"), + ), + ], + ), + ), + ), + state="replaced", + ), + ) + commands = [ + "delete nat destination rule 100", + "set nat destination rule 100 description 'Replaced DNAT rule'", + "set nat destination rule 100 protocol udp", + "set nat destination rule 100 destination address 198.51.100.10", + "set nat destination rule 100 destination port 53", + "set nat destination rule 100 translation address 192.168.1.53", + "set nat destination rule 100 translation port 53", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_replaced_cgnat_pool_partial_field_change(self): + set_module_args( + dict( + config=dict( + nat=dict( + cgnat=dict( + pool=dict( + external=[ + dict( + name="ext-pool-1", + external_port_range="30000-40000", + per_user_limit=dict(port="200"), + range=[ + dict(value="203.0.113.0/24"), + dict(value="203.1.113.1-203.1.113.60", seq="10"), + ], + ), + ], + ), + ), + ), + ), + state="replaced", + ), + ) + commands = [ + "delete nat cgnat pool external ext-pool-1", + "delete nat cgnat log-allocation", + "set nat cgnat pool external ext-pool-1 external-port-range 30000-40000", + "set nat cgnat pool external ext-pool-1 per-user-limit port 200", + "set nat cgnat pool external ext-pool-1 range 203.0.113.0/24", + "set nat cgnat pool external ext-pool-1 range 203.1.113.1-203.1.113.60 seq 10", + ] + self.execute_module(changed=True, commands=commands) + + # ------------------------------------------------------------------------- + # overridden + # ------------------------------------------------------------------------- + + def test_vyos_nat_overridden_remove_sections(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=100, + description="Overridden web server NAT", # changed + protocol="tcp", + inbound_interface=dict(name="eth3"), # changed + destination=dict(address="198.51.100.10", port="80"), + translation=dict( + address="192.168.1.10", + port="8080", + ), + ), + ], + ), + ), + ), + state="overridden", + ), + ) + commands = [ + "delete nat cgnat", + "delete nat source", + "delete nat static", + "delete nat64", + "delete nat66", + "delete nat destination rule 100", + "delete nat destination rule 105", + "set nat destination rule 100 description 'Overridden web server NAT'", + "set nat destination rule 100 protocol tcp", + "set nat destination rule 100 inbound-interface name eth3", + "set nat destination rule 100 destination address 198.51.100.10", + "set nat destination rule 100 destination port 80", + "set nat destination rule 100 translation address 192.168.1.10", + "set nat destination rule 100 translation port 8080", + ] + self.execute_module(changed=True, commands=commands) + + # ------------------------------------------------------------------------- + # rendered + # ------------------------------------------------------------------------- + + def test_vyos_nat_rendered(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=100, + description="Rendered rule", + protocol="tcp", + destination=dict(address="198.51.100.10", port="80"), + translation=dict(address="192.168.1.10", port="8080"), + ), + ], + ), + ), + ), + state="rendered", + ), + ) + rendered_cmds = [ + "set nat destination rule 100 description 'Rendered rule'", + "set nat destination rule 100 protocol tcp", + "set nat destination rule 100 destination address 198.51.100.10", + "set nat destination rule 100 destination port 80", + "set nat destination rule 100 translation address 192.168.1.10", + "set nat destination rule 100 translation port 8080", + ] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result["rendered"]), sorted(rendered_cmds), result["rendered"]) + + # ------------------------------------------------------------------------- + # parsed + # ------------------------------------------------------------------------- + + def test_vyos_nat_parsed(self): + parsed_str = ( + "set nat destination rule 100 description 'Web server NAT'\n" + "set nat destination rule 100 destination address '198.51.100.10'\n" + "set nat destination rule 100 destination port '80'\n" + "set nat destination rule 100 inbound-interface name 'eth2'\n" + "set nat destination rule 100 log\n" + "set nat destination rule 100 protocol 'tcp'\n" + "set nat destination rule 100 translation address '192.168.1.10'\n" + "set nat destination rule 100 translation port '8080'" + ) + set_module_args(dict(running_config=parsed_str, state="parsed")) + result = self.execute_module(changed=False) + parsed_list = { + "nat": { + "destination": { + "rule": [ + { + "id": 100, + "description": "Web server NAT", + "protocol": "tcp", + "log": True, + "inbound_interface": {"name": "eth2"}, + "destination": {"address": "198.51.100.10", "port": "80"}, + "translation": {"address": "192.168.1.10", "port": "8080"}, + }, + ], + }, + }, + } + self.assertEqual(parsed_list, result["parsed"]) + + # ------------------------------------------------------------------------- + # gathered + # ------------------------------------------------------------------------- + + def test_vyos_nat_gathered(self): + set_module_args(dict(state="gathered")) + result = self.execute_module(changed=False) + gathered = result["gathered"] + self.assertIn("nat", gathered) + self.assertIn("nat64", gathered) + self.assertIn("nat66", gathered) + self.assertEqual(gathered["nat"]["destination"]["rule"][0]["id"], 100) + self.assertEqual(gathered["nat64"]["source"]["rule"][0]["id"], 10) + self.assertEqual(gathered["nat66"]["destination"]["rule"][0]["id"], 20) + + def test_vyos_nat_merged_load_balance_new_rule(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=110, + protocol="tcp", + destination=dict(port="443"), + load_balance=dict( + hash=["random"], + backend=[ + dict(ip="10.20.20.1", weight=100), + ], + ), + ), + ], + ), + ), + ), + state="merged", + ), + ) + commands = [ + "set nat destination rule 110 protocol tcp", + "set nat destination rule 110 destination port 443", + "set nat destination rule 110 load-balance hash random", + "set nat destination rule 110 load-balance backend 10.20.20.1 weight 100", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_merged_load_balance_hash_add(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=105, + protocol="tcp", + destination=dict(port="80"), + inbound_interface=dict(name="eth0"), + load_balance=dict( + hash=[ + "source-address", + "destination-address", + "source-port", + ], + backend=[ + dict(ip="10.10.10.1", weight=60), + dict(ip="10.10.10.2", weight=40), + ], + ), + ), + ], + ), + ), + ), + state="merged", + ), + ) + commands = ["set nat destination rule 105 load-balance hash source-port"] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_merged_load_balance_backend_weight_change(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=105, + protocol="tcp", + destination=dict(port="80"), + inbound_interface=dict(name="eth0"), + load_balance=dict( + hash=["source-address", "destination-address"], + backend=[ + dict(ip="10.10.10.1", weight=70), + dict(ip="10.10.10.2", weight=30), + ], + ), + ), + ], + ), + ), + ), + state="merged", + ), + ) + commands = [ + "set nat destination rule 105 load-balance backend 10.10.10.1 weight 70", + "set nat destination rule 105 load-balance backend 10.10.10.2 weight 30", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_nat_replaced_load_balance_removes_omitted(self): + set_module_args( + dict( + config=dict( + nat=dict( + destination=dict( + rule=[ + dict( + id=105, + protocol="tcp", + destination=dict(port="80"), + inbound_interface=dict(name="eth0"), + load_balance=dict( + hash=["source-address"], + backend=[ + dict(ip="10.10.10.1", weight=100), + ], + ), + ), + ], + ), + ), + ), + state="replaced", + ), + ) + commands = [ + "delete nat destination rule 105", + "set nat destination rule 105 protocol tcp", + "set nat destination rule 105 destination port 80", + "set nat destination rule 105 inbound-interface name eth0", + "set nat destination rule 105 load-balance hash source-address", + "set nat destination rule 105 load-balance backend 10.10.10.1 weight 100", + ] + self.execute_module(changed=True, commands=commands) |
