diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ef0b38460..9f210428f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -36,10 +36,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: >-
- python -c "assert set([
+ python -c "assert 'failure' not in
+ set([
'${{ needs.ansible-lint.result }}',
'${{ needs.changelog.result }}',
'${{ needs.sanity.result }}',
'${{ needs.unit-galaxy.result }}',
'${{ needs.unit-source.result }}'
- ]) == {'success'}"
+ ])"
diff --git a/.gitignore b/.gitignore
index dbc2b14ed..11298d797 100644
--- a/.gitignore
+++ b/.gitignore
@@ -108,3 +108,6 @@ venv.bak/
# mypy
.mypy_cache/
+
+# vs code configuration
+.vscode/
diff --git a/README.md b/README.md
index f469ebe31..bdc08c141 100644
--- a/README.md
+++ b/README.md
@@ -44,21 +44,15 @@ Name | Description
### Filter plugins
Name | Description
--- | ---
+[ansible.netcommon.comp_type5](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.comp_type5_filter.rst)|The comp_type5 filter plugin.
+[ansible.netcommon.hash_salt](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.hash_salt_filter.rst)|The hash_salt filter plugin.
+[ansible.netcommon.parse_cli](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.parse_cli_filter.rst)|parse_cli filter plugin.
+[ansible.netcommon.parse_cli_textfsm](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.parse_cli_textfsm_filter.rst)|parse_cli_textfsm filter plugin.
+[ansible.netcommon.parse_xml](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.parse_xml_filter.rst)|The parse_xml filter plugin.
[ansible.netcommon.pop_ace](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.pop_ace_filter.rst)|Remove ace entries from a acl source of truth.
-
-### Network filter plugins
-Filters for working with output from network devices
-
-Name | Description
---- | ---
-ansible.netcommon.comp_type5|ansible.netcommon comp_type5 filter plugin
-ansible.netcommon.hash_salt|ansible.netcommon hash_salt filter plugin
-ansible.netcommon.parse_cli|ansible.netcommon parse_cli filter plugin
-ansible.netcommon.parse_cli_textfsm|ansible.netcommon parse_cli_textfsm filter plugin
-ansible.netcommon.parse_xml|ansible.netcommon parse_xml filter plugin
-ansible.netcommon.type5_pw|ansible.netcommon type5_pw filter plugin
-ansible.netcommon.vlan_expander|ansible.netcommon vlan_expander filter plugin
-ansible.netcommon.vlan_parser|Input: Unsorted list of vlan integers
+[ansible.netcommon.type5_pw](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.type5_pw_filter.rst)|The type5_pw filter plugin.
+[ansible.netcommon.vlan_expander](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.vlan_expander_filter.rst)|The vlan_expander filter plugin.
+[ansible.netcommon.vlan_parser](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.vlan_parser_filter.rst)|The vlan_parser filter plugin.
### Httpapi plugins
Name | Description
diff --git a/changelogs/fragments/vlan_extender.yaml b/changelogs/fragments/vlan_extender.yaml
new file mode 100644
index 000000000..da69abfb5
--- /dev/null
+++ b/changelogs/fragments/vlan_extender.yaml
@@ -0,0 +1,10 @@
+---
+trivial:
+ - vlan_expander - Filter plugin updated as individual plugin.
+ - vlan_parser - Filter plugin updated as individual plugin.
+ - type5_pw - Filter plugin updated as individual plugin.
+ - parse_xml - Filter plugin updated as individual plugin.
+ - comp_type5 - Filter plugin updated as individual plugin.
+ - hash_salt - Filter plugin updated as individual plugin.
+ - parse_cli - Filter plugin updated as individual plugin.
+ - parse_cli_textfsm - Filter plugin updated as individual plugin.
diff --git a/docs/ansible.netcommon.comp_type5_filter.rst b/docs/ansible.netcommon.comp_type5_filter.rst
new file mode 100644
index 000000000..130b68a71
--- /dev/null
+++ b/docs/ansible.netcommon.comp_type5_filter.rst
@@ -0,0 +1,152 @@
+.. _ansible.netcommon.comp_type5_filter:
+
+
+****************************
+ansible.netcommon.comp_type5
+****************************
+
+**The comp_type5 filter plugin.**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- The filter confirms configuration idempotency on use of type5_pw.
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Configuration |
+ Comments |
+
+
+
+
+ encrypted_password
+
+
+ string
+ / required
+
+ |
+
+ |
+
+ |
+
+ The encrypted text.
+ |
+
+
+
+
+ return_original
+
+
+ boolean
+
+ |
+
+
+ |
+
+ |
+
+ Return the original text.
+ |
+
+
+
+
+ unencrypted_password
+
+
+ string
+ / required
+
+ |
+
+ |
+
+ |
+
+ The unencrypted text.
+ |
+
+
+
+
+
+Notes
+-----
+
+.. note::
+ - The filter confirms configuration idempotency on use of type5_pw.
+ - Can be used to validate password post hashing username cisco secret 5 {{ ansible_ssh_pass | ansible.netcommon.comp_type5(encrypted, True) }}
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ # Using comp_type5
+
+ # playbook
+
+ - name: Set the facts
+ ansible.builtin.set_fact:
+ unencrypted_password: "cisco@123"
+ encrypted_password: "$1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/"
+
+ - name: Invoke comp_type5
+ ansible.builtin.debug:
+ msg: "{{ unencrypted_password | ansible.netcommon.comp_type5(encrypted_password, False) }}"
+
+ # Task Output
+ # -----------
+ #
+ # TASK [Set the facts]
+ # ok: [35.155.113.92] => changed=false
+ # ansible_facts:
+ # encrypted_password: $1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/
+ # unencrypted_password: cisco@123
+
+ # TASK [Invoke comp_type5]
+ # ok: [35.155.113.92] =>
+ # msg: true
+
+
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Ken Celenza (@itdependsnetworks)
+
+
+.. hint::
+ Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
diff --git a/docs/ansible.netcommon.hash_salt_filter.rst b/docs/ansible.netcommon.hash_salt_filter.rst
new file mode 100644
index 000000000..04e6db28a
--- /dev/null
+++ b/docs/ansible.netcommon.hash_salt_filter.rst
@@ -0,0 +1,113 @@
+.. _ansible.netcommon.hash_salt_filter:
+
+
+***************************
+ansible.netcommon.hash_salt
+***************************
+
+**The hash_salt filter plugin.**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- The filter plugin produces the salt from a hashed password.
+- Using the parameters below - ``password | ansible.netcommon.hash_salt(template.yml``)
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Configuration |
+ Comments |
+
+
+
+
+ password
+
+
+ string
+ / required
+
+ |
+
+ |
+
+ |
+
+ This source data on which hash_salt invokes.
+ For example password | ansible.netcommon.hash_salt , in this case password represents the hashed password.
+ |
+
+
+
+
+
+Notes
+-----
+
+.. note::
+ - The filter plugin produces the salt from a hashed password.
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ # Using hash_salt
+
+ # playbook
+
+ - name: Set the facts
+ ansible.builtin.set_fact:
+ password: "$1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/"
+
+ - name: Invoke hash_salt
+ ansible.builtin.debug:
+ msg: "{{ password | ansible.netcommon.hash_salt() }}"
+
+
+ # Task Output
+ # -----------
+ #
+ # TASK [Set the facts]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # password: $1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/
+
+ # TASK [Invoke hash_salt]
+ # ok: [host] =>
+ # msg: avs
+
+
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Ken Celenza (@itdependsnetworks)
+
+
+.. hint::
+ Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
diff --git a/docs/ansible.netcommon.parse_cli_filter.rst b/docs/ansible.netcommon.parse_cli_filter.rst
new file mode 100644
index 000000000..0c0069af1
--- /dev/null
+++ b/docs/ansible.netcommon.parse_cli_filter.rst
@@ -0,0 +1,191 @@
+.. _ansible.netcommon.parse_cli_filter:
+
+
+***************************
+ansible.netcommon.parse_cli
+***************************
+
+**parse_cli filter plugin.**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- The filter plugins converts the output of a network device CLI command into structured JSON output.
+- Using the parameters below - ``xml_data | ansible.netcommon.parse_cli(template.yml``)
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Configuration |
+ Comments |
+
+
+
+
+ output
+
+
+ raw
+ / required
+
+ |
+
+ |
+
+ |
+
+ This source data on which parse_cli invokes.
+ |
+
+
+
+
+ tmpl
+
+
+ string
+
+ |
+
+ |
+
+ |
+
+ The spec file should be valid formatted YAML. It defines how to parse the CLI output and return JSON data.
+ For example cli_data | ansible.netcommon.parse_cli(template.yml ), in this case cli_data represents cli output.
+ |
+
+
+
+
+
+Notes
+-----
+
+.. note::
+ - The parse_cli filter will load the spec file and pass the command output through it, returning JSON output. The YAML spec file defines how to parse the CLI output
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ # Using parse_cli
+
+ # outputConfig
+
+ # ip dhcp pool Data
+ # import all
+ # network 192.168.1.0 255.255.255.0
+ # update dns
+ # default-router 192.168.1.1
+ # dns-server 192.168.1.1 8.8.8.8
+ # option 42 ip 192.168.1.1
+ # domain-name test.local
+ # lease 8
+
+ # pconnection.yml
+
+ # ---
+ # vars:
+ # dhcp_pool:
+ # name: "{{ item.name }}"
+ # network: "{{ item.network_ip }}"
+ # subnet: "{{ item.network_subnet }}"
+ # dns_servers: "{{ item.dns_servers_1 }}{{ item.dns_servers_2 }}"
+ # domain_name: "{{ item.domain_name_0 }}{{ item.domain_name_1 }}{{ item.domain_name_2 }}{{ item.domain_name_3 }}"
+ # options: "{{ item.options_1 }}{{ item.options_2 }}"
+ # lease_days: "{{ item.lease_days }}"
+ # lease_hours: "{{ item.lease_hours }}"
+ # lease_minutes: "{{ item.lease_minutes }}"
+
+ # keys:
+ # dhcp_pools:
+ # value: "{{ dhcp_pool }}"
+ # items: "^ip dhcp pool (
+ # ?P[^\\n]+)\\s+(?:import (?Pall)\\s*)?(?:network (?P[\\d.]+)
+ # (?P[\\d.]+)?\\s*)?(?:update dns\\s*)?(?:host (?P[\\d.]+)
+ # (?P[\\d.]+)\\s*)?(?:domain-name (?P[\\w._-]+)\\s+)?
+ # (?:default-router (?P[\\d.]+)\\s*)?(?:dns-server
+ # (?P(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name (?P[\\w._-]+)\\s+)?
+ # (?P(?:option [^\\n]+\\n*\\s*)*)?(?:domain-name (?P[\\w._-]+)\\s+)?(?P(?:option [^\\n]+\\n*\\s*)*)?
+ # (?:dns-server (?P(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name
+ # (?P[\\w._-]+)\\s*)?(lease (?P\\d+)(?: (?P\\d+))?(?: (?P\\d+))?\\s*)?(?:update arp)?"
+
+ # playbook
+
+ - name: Add config data
+ ansible.builtin.set_fact:
+ opconfig: "{{lookup('ansible.builtin.file', 'outputConfig') }}"
+
+ - name: Parse Data
+ ansible.builtin.set_fact:
+ output: "{{ opconfig | parse_cli('pconnection.yml') }}"
+
+
+ # Task Output
+ # -----------
+ #
+ # TASK [Add config data]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # xml: |-
+ # ip dhcp pool Data
+ # import all
+ # network 192.168.1.0 255.255.255.0
+ # update dns
+ # default-router 192.168.1.1
+ # dns-server 192.168.1.1 8.8.8.8
+ # option 42 ip 192.168.1.1
+ # domain-name test.local
+ # lease 8
+
+ # TASK [Parse Data]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # output:
+ # dhcp_pools:
+ # - dns_servers: 192.168.1.1 8.8.8.8
+ # domain_name: test.local
+ # lease_days: 8
+ # lease_hours: null
+ # lease_minutes: null
+ # name: Data
+ # network: 192.168.1.0
+ # options: |-
+ # option 42 ip 192.168.1.1
+ # subnet: 255.255.255.0
+
+
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Peter Sprygada (@privateip)
+
+
+.. hint::
+ Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
diff --git a/docs/ansible.netcommon.parse_cli_textfsm_filter.rst b/docs/ansible.netcommon.parse_cli_textfsm_filter.rst
new file mode 100644
index 000000000..9f2283f5d
--- /dev/null
+++ b/docs/ansible.netcommon.parse_cli_textfsm_filter.rst
@@ -0,0 +1,148 @@
+.. _ansible.netcommon.parse_cli_textfsm_filter:
+
+
+***********************************
+ansible.netcommon.parse_cli_textfsm
+***********************************
+
+**parse_cli_textfsm filter plugin.**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- The network filters also support parsing the output of a CLI command using the TextFSM library. To parse the CLI output with TextFSM use this filter.
+- Using the parameters below - ``data | ansible.netcommon.parse_cli_textfsm(template.yml``)
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Configuration |
+ Comments |
+
+
+
+
+ template
+
+
+ string
+
+ |
+
+ |
+
+ |
+
+ The template to compare it with.
+ For example data | ansible.netcommon.parse_cli_textfsm(template.yml ), in this case data represents this option.
+ |
+
+
+
+
+ value
+
+
+ raw
+ / required
+
+ |
+
+ |
+
+ |
+
+ This source data on which parse_cli_textfsm invokes.
+ |
+
+
+
+
+
+Notes
+-----
+
+.. note::
+ - Use of the TextFSM filter requires the TextFSM library to be installed.
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ # Using parse_cli_textfsm
+
+ - name: "Fetch command output"
+ cisco.ios.ios_command:
+ commands:
+ - show lldp neighbors
+ register: lldp_output
+
+ - name: "Invoke parse_cli_textfsm"
+ ansible.builtin.set_fact:
+ device_neighbors: "{{ lldp_output.stdout[0] | parse_cli_textfsm('~/ntc-templates/templates/cisco_ios_show_lldp_neighbors.textfsm') }}"
+
+ - name: "Debug"
+ ansible.builtindebug:
+ msg: "{{ device_neighbors }}"
+
+ # Task Output
+ # -----------
+ #
+ # TASK [Fetch command output]
+ # ok: [rtr-2]
+
+ # TASK [Invoke parse_cli_textfsm]
+ # ok: [rtr-1]
+
+ # TASK [Debug]
+ # ok: [rtr-1] => {
+ # "msg": [
+ # {
+ # "CAPABILITIES": "R",
+ # "LOCAL_INTERFACE": "Gi0/0",
+ # "NEIGHBOR": "rtr-3",
+ # "NEIGHBOR_INTERFACE": "Gi0/0"
+ # },
+ # {
+ # "CAPABILITIES": "R",
+ # "LOCAL_INTERFACE": "Gi0/1",
+ # "NEIGHBOR": "rtr-1",
+ # "NEIGHBOR_INTERFACE": "Gi0/1"
+ # }
+ # ]
+ # }
+
+
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Peter Sprygada (@privateip)
+
+
+.. hint::
+ Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
diff --git a/docs/ansible.netcommon.parse_xml_filter.rst b/docs/ansible.netcommon.parse_xml_filter.rst
new file mode 100644
index 000000000..487d79b82
--- /dev/null
+++ b/docs/ansible.netcommon.parse_xml_filter.rst
@@ -0,0 +1,223 @@
+.. _ansible.netcommon.parse_xml_filter:
+
+
+***************************
+ansible.netcommon.parse_xml
+***************************
+
+**The parse_xml filter plugin.**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- This filter will load the spec file and pass the command output through it, returning JSON output.
+- The YAML spec file defines how to parse the CLI output.
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Configuration |
+ Comments |
+
+
+
+
+ output
+
+
+ raw
+ / required
+
+ |
+
+ |
+
+ |
+
+ This source xml on which parse_xml invokes.
+ |
+
+
+
+
+ tmpl
+
+
+ string
+
+ |
+
+ |
+
+ |
+
+ The spec file should be valid formatted YAML. It defines how to parse the XML output and return JSON data.
+ For example xml_data | ansible.netcommon.parse_xml(template.yml ), in this case xml_data represents xml data option.
+ |
+
+
+
+
+
+Notes
+-----
+
+.. note::
+ - To convert the XML output of a network device command into structured JSON output.
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ # Using parse_xml
+
+ # example_output.xml
+
+ #
+ #
+ #
+ #
+ #
+ #
+ # 0/0/CPU0
+ #
+ # true
+ # ntp-leap-no-warning
+ #
+ #
+ # ntp-mode-client
+ # true
+ # 10.1.1.1
+ # 0
+ #
+ # -1
+ #
+ #
+ #
+ # ntp-mode-client
+ # true
+ # 172.16.252.29
+ # 255
+ #
+ # 991
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+
+ # parse_xml.yml
+
+ # ---
+ # vars:
+ # ntp_peers:
+ # address: "{{ item.address }}"
+ # reachability: "{{ item.reachability}}"
+ # keys:
+ # result:
+ # value: "{{ ntp_peers }}"
+ # top: data/ntp/nodes/node/associations
+ # items:
+ # address: peer-summary-info/peer-info-common/address
+ # reachability: peer-summary-info/peer-info-common/reachability
+
+
+ - name: Facts setup
+ ansible.builtin.set_fact:
+ xml: "{{ lookup('file', 'example_output.xml') }}"
+
+ - name: Parse xml invocation
+ ansible.builtin.debug:
+ msg: "{{ xml | ansible.netcommon.parse_xml('parse_xml.yml') }}"
+
+
+ # Task Output
+ # -----------
+ #
+ # TASK [set xml Data]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # xml: |-
+ #
+ #
+ #
+ #
+ #
+ #
+ # 0/0/CPU0
+ #
+ # true
+ # ntp-leap-no-warning
+ #
+ #
+ # ntp-mode-client
+ # true
+ # 10.1.1.1
+ # 0
+ #
+ # -1
+ #
+ #
+ #
+ # ntp-mode-client
+ # true
+ # 172.16.252.29
+ # 255
+ #
+ # 991
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+
+ # TASK [Parse Data]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # output:
+ # result:
+ # - address:
+ # - 10.1.1.1
+ # - 172.16.252.29
+ # reachability:
+ # - '0'
+ # - '255'
+
+
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Ganesh Nalawade (@ganeshrn)
+
+
+.. hint::
+ Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
diff --git a/docs/ansible.netcommon.type5_pw_filter.rst b/docs/ansible.netcommon.type5_pw_filter.rst
new file mode 100644
index 000000000..a7fe830bc
--- /dev/null
+++ b/docs/ansible.netcommon.type5_pw_filter.rst
@@ -0,0 +1,127 @@
+.. _ansible.netcommon.type5_pw_filter:
+
+
+**************************
+ansible.netcommon.type5_pw
+**************************
+
+**The type5_pw filter plugin.**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- Filter plugin to produce cisco type5 hashed password.
+- Using the parameters below - ``xml_data | ansible.netcommon.type5_pw(template.yml``)
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Configuration |
+ Comments |
+
+
+
+
+ password
+
+
+ string
+ / required
+
+ |
+
+ |
+
+ |
+
+ The password to be hashed.
+ |
+
+
+
+
+ salt
+
+
+ string
+
+ |
+
+ |
+
+ |
+
+ Mention the salt to hash the password.
+ |
+
+
+
+
+
+Notes
+-----
+
+.. note::
+ - The filter plugin generates cisco type5 hashed password.
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ # Using type5_pw
+
+ - name: Set some facts
+ ansible.builtin.set_fact:
+ password: "cisco@123"
+
+ - name: Filter type5_pw invocation
+ ansible.builtin.debug:
+ msg: "{{ password | ansible.netcommon.type5_pw(salt='avs') }}"
+
+
+ # Task Output
+ # -----------
+ #
+ # TASK [Set some facts]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # password: cisco@123
+
+ # TASK [Filter type5_pw invocation]
+ # ok: [host] =>
+ # msg: $1$avs$uSTOEMh65qzvpb9yBMpzd/
+
+
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Ken Celenza (@itdependsnetworks)
+
+
+.. hint::
+ Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
diff --git a/docs/ansible.netcommon.vlan_expander_filter.rst b/docs/ansible.netcommon.vlan_expander_filter.rst
new file mode 100644
index 000000000..bcde65b03
--- /dev/null
+++ b/docs/ansible.netcommon.vlan_expander_filter.rst
@@ -0,0 +1,119 @@
+.. _ansible.netcommon.vlan_expander_filter:
+
+
+*******************************
+ansible.netcommon.vlan_expander
+*******************************
+
+**The vlan_expander filter plugin.**
+
+
+Version added: 2.3.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- Expand shorthand list of VLANs to list all VLANs. Inverse of vlan_parser
+- Using the parameters below - ``vlans_data | ansible.netcommon.vlan_expander``
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Configuration |
+ Comments |
+
+
+
+
+ data
+
+
+ string
+ / required
+
+ |
+
+ |
+
+ |
+
+ This option represents a string containing the range of vlans.
+ |
+
+
+
+
+
+Notes
+-----
+
+.. note::
+ - The filter plugin extends vlans when data provided in range or comma separated.
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ # Using vlan_expander
+
+ - name: Setting host facts for vlan_expander filter plugin
+ ansible.builtin.set_fact:
+ vlan_ranges: "1,10-12,15,20-22"
+
+ - name: Invoke vlan_expander filter plugin
+ ansible.builtin.set_fact:
+ extended_vlans: "{{ vlan_ranges | ansible.netcommon.vlan_expander }}"
+
+
+ # Task Output
+ # -----------
+ #
+ # TASK [Setting host facts for vlan_expander filter plugin]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # vlan_ranges: 1,10-12,15,20-22
+
+ # TASK [Invoke vlan_expander filter plugin]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # extended_vlans:
+ # - 1
+ # - 10
+ # - 11
+ # - 12
+ # - 15
+ # - 20
+ # - 21
+ # - 22
+
+
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Akira Yokochi (@akira6592)
+
+
+.. hint::
+ Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
diff --git a/docs/ansible.netcommon.vlan_parser_filter.rst b/docs/ansible.netcommon.vlan_parser_filter.rst
new file mode 100644
index 000000000..43419d06a
--- /dev/null
+++ b/docs/ansible.netcommon.vlan_parser_filter.rst
@@ -0,0 +1,181 @@
+.. _ansible.netcommon.vlan_parser_filter:
+
+
+*****************************
+ansible.netcommon.vlan_parser
+*****************************
+
+**The vlan_parser filter plugin.**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- The filter plugin converts a list of vlans to IOS like vlan configuration.
+- Converts list to a list of range of numbers into multiple lists.
+- ``vlans_data | ansible.netcommon.vlan_parser(first_line_len = 20, other_line_len=20``)
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Configuration |
+ Comments |
+
+
+
+
+ data
+
+
+ list
+ / required
+
+ |
+
+ |
+
+ |
+
+ This option represents a list containing vlans.
+ |
+
+
+
+
+ first_line_len
+
+
+ integer
+
+ |
+
+ Default:
48
+ |
+
+ |
+
+ The first line of the list can be first_line_len characters long.
+ |
+
+
+
+
+ other_line_len
+
+
+ integer
+
+ |
+
+ Default:
44
+ |
+
+ |
+
+ The subsequent list lines can be other_line_len characters.
+ |
+
+
+
+
+
+Notes
+-----
+
+.. note::
+ - The filter plugin extends vlans when data provided in range or comma separated.
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ # Using vlan_parser
+
+ - name: Setting host facts for vlan_parser filter plugin
+ ansible.builtin.set_fact:
+ vlans:
+ [
+ 100,
+ 1688,
+ 3002,
+ 3003,
+ 3004,
+ 3005,
+ 3102,
+ 3103,
+ 3104,
+ 3105,
+ 3802,
+ 3900,
+ 3998,
+ 3999,
+ ]
+
+ - name: Invoke vlan_parser filter plugin
+ ansible.builtin.set_fact:
+ vlans_ranges: "{{ vlans | ansible.netcommon.vlan_parser(first_line_len = 20, other_line_len=20) }}"
+
+
+ # Task Output
+ # -----------
+ #
+ # TASK [Setting host facts for vlan_parser filter plugin]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # vlans:
+ # - 100
+ # - 1688
+ # - 3002
+ # - 3003
+ # - 3004
+ # - 3005
+ # - 3102
+ # - 3103
+ # - 3104
+ # - 3105
+ # - 3802
+ # - 3900
+ # - 3998
+ # - 3999
+
+ # TASK [Invoke vlan_parser filter plugin]
+ # ok: [host] => changed=false
+ # ansible_facts:
+ # msg:
+ # - 100,1688,3002-3005
+ # - 3102-3105,3802,3900
+ # - 3998,3999
+
+
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Steve Dodd (@idahood)
+
+
+.. hint::
+ Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
diff --git a/plugins/filter/comp_type5.py b/plugins/filter/comp_type5.py
new file mode 100644
index 000000000..aaa36f5ac
--- /dev/null
+++ b/plugins/filter/comp_type5.py
@@ -0,0 +1,109 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The comp_type5 filter plugin
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+name: comp_type5
+author: Ken Celenza (@itdependsnetworks)
+version_added: "1.0.0"
+short_description: The comp_type5 filter plugin.
+description:
+ - The filter confirms configuration idempotency on use of type5_pw.
+notes:
+ - The filter confirms configuration idempotency on use of type5_pw.
+ - Can be used to validate password post hashing
+ username cisco secret 5 {{ ansible_ssh_pass | ansible.netcommon.comp_type5(encrypted, True) }}
+options:
+ unencrypted_password:
+ description:
+ - The unencrypted text.
+ type: str
+ required: True
+ encrypted_password:
+ description:
+ - The encrypted text.
+ type: str
+ required: True
+ return_original:
+ description:
+ - Return the original text.
+ type: bool
+"""
+
+EXAMPLES = r"""
+# Using comp_type5
+
+# playbook
+
+- name: Set the facts
+ ansible.builtin.set_fact:
+ unencrypted_password: "cisco@123"
+ encrypted_password: "$1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/"
+
+- name: Invoke comp_type5
+ ansible.builtin.debug:
+ msg: "{{ unencrypted_password | ansible.netcommon.comp_type5(encrypted_password, False) }}"
+
+# Task Output
+# -----------
+#
+# TASK [Set the facts]
+# ok: [35.155.113.92] => changed=false
+# ansible_facts:
+# encrypted_password: $1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/
+# unencrypted_password: cisco@123
+
+# TASK [Invoke comp_type5]
+# ok: [35.155.113.92] =>
+# msg: true
+"""
+
+from ansible.errors import AnsibleFilterError
+from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
+ AnsibleArgSpecValidator,
+)
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.comp_type5 import comp_type5
+
+
+try:
+ from jinja2.filters import pass_environment
+except ImportError:
+ from jinja2.filters import environmentfilter as pass_environment
+
+
+@pass_environment
+def _comp_type5(*args, **kwargs):
+ """Extend vlan data"""
+
+ keys = [
+ "unencrypted_password",
+ "encrypted_password",
+ "return_original",
+ ]
+ data = dict(zip(keys, args[1:]))
+ data.update(kwargs)
+ aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="comp_type5")
+ valid, errors, updated_data = aav.validate()
+ if not valid:
+ raise AnsibleFilterError(errors)
+ return comp_type5(**updated_data)
+
+
+class FilterModule(object):
+ """comp_type5"""
+
+ def filters(self):
+ """a mapping of filter names to functions"""
+ return {"comp_type5": _comp_type5}
diff --git a/plugins/filter/hash_salt.py b/plugins/filter/hash_salt.py
new file mode 100644
index 000000000..6634d78b6
--- /dev/null
+++ b/plugins/filter/hash_salt.py
@@ -0,0 +1,96 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The hash_salt filter plugin
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+name: hash_salt
+author: Ken Celenza (@itdependsnetworks)
+version_added: "1.0.0"
+short_description: The hash_salt filter plugin.
+description:
+ - The filter plugin produces the salt from a hashed password.
+ - Using the parameters below - C(password | ansible.netcommon.hash_salt(template.yml))
+notes:
+ - The filter plugin produces the salt from a hashed password.
+options:
+ password:
+ description:
+ - This source data on which hash_salt invokes.
+ - For example C(password | ansible.netcommon.hash_salt),
+ in this case C(password) represents the hashed password.
+ type: str
+ required: True
+"""
+
+EXAMPLES = r"""
+# Using hash_salt
+
+# playbook
+
+- name: Set the facts
+ ansible.builtin.set_fact:
+ password: "$1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/"
+
+- name: Invoke hash_salt
+ ansible.builtin.debug:
+ msg: "{{ password | ansible.netcommon.hash_salt() }}"
+
+
+# Task Output
+# -----------
+#
+# TASK [Set the facts]
+# ok: [host] => changed=false
+# ansible_facts:
+# password: $1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/
+
+# TASK [Invoke hash_salt]
+# ok: [host] =>
+# msg: avs
+"""
+
+from ansible.errors import AnsibleFilterError
+from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
+ AnsibleArgSpecValidator,
+)
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.hash_salt import hash_salt
+
+
+try:
+ from jinja2.filters import pass_environment
+except ImportError:
+ from jinja2.filters import environmentfilter as pass_environment
+
+
+@pass_environment
+def _hash_salt(*args, **kwargs):
+ """Extend vlan data"""
+
+ keys = ["password"]
+ data = dict(zip(keys, args[1:]))
+ data.update(kwargs)
+ aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="hash_salt")
+ valid, errors, updated_data = aav.validate()
+ if not valid:
+ raise AnsibleFilterError(errors)
+ return hash_salt(**updated_data)
+
+
+class FilterModule(object):
+ """hash_salt"""
+
+ def filters(self):
+ """a mapping of filter names to functions"""
+ return {"hash_salt": _hash_salt}
diff --git a/plugins/filter/network.py b/plugins/filter/network.py
deleted file mode 100644
index dd287a8bc..000000000
--- a/plugins/filter/network.py
+++ /dev/null
@@ -1,513 +0,0 @@
-#
-# {c) 2017 Red Hat, Inc.
-# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-# Make coding more python3-ish
-from __future__ import absolute_import, division, print_function
-
-
-__metaclass__ = type
-
-import os
-import re
-import string
-import traceback
-
-from xml.etree.ElementTree import fromstring
-
-from ansible.errors import AnsibleError, AnsibleFilterError
-from ansible.module_utils._text import to_native, to_text
-from ansible.module_utils.common._collections_compat import Mapping
-from ansible.module_utils.six import iteritems, string_types
-from ansible.utils.display import Display
-from ansible.utils.encrypt import passlib_or_crypt, random_password
-
-from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import Template
-
-
-try:
- import yaml
-
- HAS_YAML = True
-except ImportError:
- HAS_YAML = False
-
-try:
- import textfsm
-
- HAS_TEXTFSM = True
-except ImportError:
- HAS_TEXTFSM = False
-
-display = Display()
-
-
-def re_matchall(regex, value):
- objects = list()
- for match in re.findall(regex.pattern, value, re.M):
- obj = {}
- if regex.groupindex:
- for name, index in iteritems(regex.groupindex):
- if len(regex.groupindex) == 1:
- obj[name] = match
- else:
- obj[name] = match[index - 1]
- objects.append(obj)
- return objects
-
-
-def re_search(regex, value):
- obj = {}
- match = regex.search(value, re.M)
- if match:
- items = list(match.groups())
- if regex.groupindex:
- for name, index in iteritems(regex.groupindex):
- obj[name] = items[index - 1]
- return obj
-
-
-def re_finditer(regex, value):
- iter_obj = re.finditer(regex, value)
- values = None
- for each in iter_obj:
- if not values:
- values = each.groupdict()
- else:
- # for backward compatibility
- values.update(each.groupdict())
- # for backward compatibility
- values["match"] = list(each.groups())
- groups = each.groupdict()
- for group in groups:
- if not values.get("match_all"):
- values["match_all"] = dict()
- if not values["match_all"].get(group):
- values["match_all"][group] = list()
- values["match_all"][group].append(groups[group])
- return values
-
-
-def parse_cli(output, tmpl):
- if not isinstance(output, string_types):
- raise AnsibleError(
- "parse_cli input should be a string, but was given a input of %s" % (type(output))
- )
-
- if not os.path.exists(tmpl):
- raise AnsibleError("unable to locate parse_cli template: %s" % tmpl)
-
- try:
- template = Template()
- except ImportError as exc:
- raise AnsibleError(to_native(exc))
-
- with open(tmpl) as tmpl_fh:
- tmpl_content = tmpl_fh.read()
-
- spec = yaml.safe_load(tmpl_content)
- obj = {}
-
- for name, attrs in iteritems(spec["keys"]):
- value = attrs["value"]
-
- try:
- variables = spec.get("vars", {})
- value = template(value, variables)
- except Exception:
- pass
-
- if "start_block" in attrs and "end_block" in attrs:
- start_block = re.compile(attrs["start_block"])
- end_block = re.compile(attrs["end_block"])
-
- blocks = list()
- lines = None
- block_started = False
-
- for line in output.split("\n"):
- match_start = start_block.match(line)
- match_end = end_block.match(line)
-
- if match_start:
- lines = list()
- lines.append(line)
- block_started = True
-
- elif match_end:
- if lines:
- lines.append(line)
- blocks.append("\n".join(lines))
- lines = None
- block_started = False
-
- elif block_started:
- if lines:
- lines.append(line)
-
- regex_items = [re.compile(r) for r in attrs["items"]]
- objects = list()
-
- for block in blocks:
- if isinstance(value, Mapping) and "key" not in value:
- items = list()
- for regex in regex_items:
- items.append(re_finditer(regex, block))
-
- obj = {}
- for k, v in iteritems(value):
- try:
- obj[k] = template(v, {"item": items}, fail_on_undefined=False)
- except Exception:
- obj[k] = None
- objects.append(obj)
-
- elif isinstance(value, Mapping):
- items = list()
- for regex in regex_items:
- items.append(re_finditer(regex, block))
-
- key = template(value["key"], {"item": items})
- values = dict(
- [(k, template(v, {"item": items})) for k, v in iteritems(value["values"])]
- )
- objects.append({key: values})
-
- return objects
-
- elif "items" in attrs:
- regexp = re.compile(attrs["items"])
- when = attrs.get("when")
- conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" % when
-
- if isinstance(value, Mapping) and "key" not in value:
- values = list()
-
- for item in re_matchall(regexp, output):
- entry = {}
-
- for item_key, item_value in iteritems(value):
- entry[item_key] = template(item_value, {"item": item})
-
- if when:
- if template(conditional, {"item": entry}):
- values.append(entry)
- else:
- values.append(entry)
-
- obj[name] = values
-
- elif isinstance(value, Mapping):
- values = dict()
-
- for item in re_matchall(regexp, output):
- entry = {}
-
- for item_key, item_value in iteritems(value["values"]):
- entry[item_key] = template(item_value, {"item": item})
-
- key = template(value["key"], {"item": item})
-
- if when:
- if template(conditional, {"item": {"key": key, "value": entry}}):
- values[key] = entry
- else:
- values[key] = entry
-
- obj[name] = values
-
- else:
- item = re_search(regexp, output)
- obj[name] = template(value, {"item": item})
-
- else:
- obj[name] = value
-
- return obj
-
-
-def parse_cli_textfsm(value, template):
- if not HAS_TEXTFSM:
- raise AnsibleError("parse_cli_textfsm filter requires TextFSM library to be installed")
-
- if not isinstance(value, string_types):
- raise AnsibleError(
- "parse_cli_textfsm input should be a string, but was given a input of %s"
- % (type(value))
- )
-
- if not os.path.exists(template):
- raise AnsibleError("unable to locate parse_cli_textfsm template: %s" % template)
-
- try:
- template = open(template)
- except IOError as exc:
- raise AnsibleError(to_native(exc))
-
- re_table = textfsm.TextFSM(template)
- fsm_results = re_table.ParseText(value)
-
- results = list()
- for item in fsm_results:
- results.append(dict(zip(re_table.header, item)))
-
- return results
-
-
-def _extract_param(template, root, attrs, value):
- key = None
- when = attrs.get("when")
- conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" % when
- param_to_xpath_map = attrs["items"]
-
- if isinstance(value, Mapping):
- key = value.get("key", None)
- if key:
- value = value["values"]
-
- entries = dict() if key else list()
-
- for element in root.findall(attrs["top"]):
- entry = dict()
- item_dict = dict()
- for param, param_xpath in iteritems(param_to_xpath_map):
- fields = None
- try:
- fields = element.findall(param_xpath)
- except Exception:
- display.warning(
- "Failed to evaluate value of '%s' with XPath '%s'.\nUnexpected error: %s."
- % (param, param_xpath, traceback.format_exc())
- )
-
- tags = param_xpath.split("/")
-
- # check if xpath ends with attribute.
- # If yes set attribute key/value dict to param value in case attribute matches
- # else if it is a normal xpath assign matched element text value.
- if len(tags) and tags[-1].endswith("]"):
- if fields:
- if len(fields) > 1:
- item_dict[param] = [field.attrib for field in fields]
- else:
- item_dict[param] = fields[0].attrib
- else:
- item_dict[param] = {}
- else:
- if fields:
- if len(fields) > 1:
- item_dict[param] = [field.text for field in fields]
- else:
- item_dict[param] = fields[0].text
- else:
- item_dict[param] = None
-
- if isinstance(value, Mapping):
- for item_key, item_value in iteritems(value):
- entry[item_key] = template(item_value, {"item": item_dict})
- else:
- entry = template(value, {"item": item_dict})
-
- if key:
- expanded_key = template(key, {"item": item_dict})
- if when:
- if template(
- conditional,
- {"item": {"key": expanded_key, "value": entry}},
- ):
- entries[expanded_key] = entry
- else:
- entries[expanded_key] = entry
- else:
- if when:
- if template(conditional, {"item": entry}):
- entries.append(entry)
- else:
- entries.append(entry)
-
- return entries
-
-
-def parse_xml(output, tmpl):
- if not os.path.exists(tmpl):
- raise AnsibleError("unable to locate parse_xml template: %s" % tmpl)
-
- if not isinstance(output, string_types):
- raise AnsibleError(
- "parse_xml works on string input, but given input of : %s" % type(output)
- )
-
- root = fromstring(output)
- try:
- template = Template()
- except ImportError as exc:
- raise AnsibleError(to_native(exc))
-
- with open(tmpl) as tmpl_fh:
- tmpl_content = tmpl_fh.read()
-
- spec = yaml.safe_load(tmpl_content)
- obj = {}
-
- for name, attrs in iteritems(spec["keys"]):
- value = attrs["value"]
-
- try:
- variables = spec.get("vars", {})
- value = template(value, variables)
- except Exception:
- pass
-
- if "items" in attrs:
- obj[name] = _extract_param(template, root, attrs, value)
- else:
- obj[name] = value
-
- return obj
-
-
-def type5_pw(password, salt=None):
- if not isinstance(password, string_types):
- raise AnsibleFilterError(
- "type5_pw password input should be a string, but was given a input of %s"
- % (type(password).__name__)
- )
-
- salt_chars = "".join((to_text(string.ascii_letters), to_text(string.digits), "./"))
- if salt is not None and not isinstance(salt, string_types):
- raise AnsibleFilterError(
- "type5_pw salt input should be a string, but was given a input of %s"
- % (type(salt).__name__)
- )
- elif not salt:
- salt = random_password(length=4, chars=salt_chars)
- elif not set(salt) <= set(salt_chars):
- raise AnsibleFilterError(
- "type5_pw salt used inproper characters, must be one of %s" % (salt_chars)
- )
-
- encrypted_password = passlib_or_crypt(password, "md5_crypt", salt=salt)
-
- return encrypted_password
-
-
-def hash_salt(password):
- split_password = password.split("$")
- if len(split_password) != 4:
- raise AnsibleFilterError(
- "Could not parse salt out password correctly from {0}".format(password)
- )
- else:
- return split_password[2]
-
-
-def comp_type5(unencrypted_password, encrypted_password, return_original=False):
- salt = hash_salt(encrypted_password)
- if type5_pw(unencrypted_password, salt) == encrypted_password:
- if return_original is True:
- return encrypted_password
- else:
- return True
- return False
-
-
-def vlan_expander(raw_vlan):
- expanded_list = []
- for each in raw_vlan.split(","):
- if "-" in each:
- f, t = map(int, each.split("-"))
- expanded_list.extend(range(f, t + 1))
- else:
- expanded_list.append(int(each))
- return sorted(expanded_list)
-
-
-def vlan_parser(vlan_list, first_line_len=48, other_line_len=44):
- """
- Input: Unsorted list of vlan integers
- Output: Sorted string list of integers according to IOS-like vlan list rules
-
- 1. Vlans are listed in ascending order
- 2. Runs of 3 or more consecutive vlans are listed with a dash
- 3. The first line of the list can be first_line_len characters long
- 4. Subsequent list lines can be other_line_len characters
- """
-
- # Sort and remove duplicates
- sorted_list = sorted(set(vlan_list))
-
- if sorted_list[0] < 1 or sorted_list[-1] > 4094:
- raise AnsibleFilterError("Valid VLAN range is 1-4094")
-
- parse_list = []
- idx = 0
- while idx < len(sorted_list):
- start = idx
- end = start
- while end < len(sorted_list) - 1:
- if sorted_list[end + 1] - sorted_list[end] == 1:
- end += 1
- else:
- break
-
- if start == end:
- # Single VLAN
- parse_list.append(str(sorted_list[idx]))
- elif start + 1 == end:
- # Run of 2 VLANs
- parse_list.append(str(sorted_list[start]))
- parse_list.append(str(sorted_list[end]))
- else:
- # Run of 3 or more VLANs
- parse_list.append(str(sorted_list[start]) + "-" + str(sorted_list[end]))
- idx = end + 1
-
- line_count = 0
- result = [""]
- for vlans in parse_list:
- # First line (" switchport trunk allowed vlan ")
- if line_count == 0:
- if len(result[line_count] + vlans) > first_line_len:
- result.append("")
- line_count += 1
- result[line_count] += vlans + ","
- else:
- result[line_count] += vlans + ","
-
- # Subsequent lines (" switchport trunk allowed vlan add ")
- else:
- if len(result[line_count] + vlans) > other_line_len:
- result.append("")
- line_count += 1
- result[line_count] += vlans + ","
- else:
- result[line_count] += vlans + ","
-
- # Remove trailing orphan commas
- for idx in range(0, len(result)):
- result[idx] = result[idx].rstrip(",")
-
- # Sometimes text wraps to next line, but there are no remaining VLANs
- if "" in result:
- result.remove("")
-
- return result
-
-
-class FilterModule(object):
- """Filters for working with output from network devices"""
-
- filter_map = {
- "parse_cli": parse_cli,
- "parse_cli_textfsm": parse_cli_textfsm,
- "parse_xml": parse_xml,
- "type5_pw": type5_pw,
- "hash_salt": hash_salt,
- "comp_type5": comp_type5,
- "vlan_parser": vlan_parser,
- "vlan_expander": vlan_expander,
- }
-
- def filters(self):
- return self.filter_map
diff --git a/plugins/filter/parse_cli.py b/plugins/filter/parse_cli.py
new file mode 100644
index 000000000..c334a8d40
--- /dev/null
+++ b/plugins/filter/parse_cli.py
@@ -0,0 +1,164 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The parse_cli filter plugin
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+name: parse_cli
+author: Peter Sprygada (@privateip)
+version_added: "1.0.0"
+short_description: parse_cli filter plugin.
+description:
+ - The filter plugins converts the output of a network device
+ CLI command into structured JSON output.
+ - Using the parameters below - C(xml_data | ansible.netcommon.parse_cli(template.yml))
+notes:
+ - The parse_cli filter will load the spec file and pass the command output through it,
+ returning JSON output. The YAML spec file defines how to parse the CLI output
+options:
+ output:
+ description:
+ - This source data on which parse_cli invokes.
+ type: raw
+ required: True
+ tmpl:
+ description:
+ - The spec file should be valid formatted YAML.
+ It defines how to parse the CLI output and return JSON data.
+ - For example C(cli_data | ansible.netcommon.parse_cli(template.yml)),
+ in this case C(cli_data) represents cli output.
+ type: string
+"""
+
+EXAMPLES = r"""
+# Using parse_cli
+
+# outputConfig
+
+# ip dhcp pool Data
+# import all
+# network 192.168.1.0 255.255.255.0
+# update dns
+# default-router 192.168.1.1
+# dns-server 192.168.1.1 8.8.8.8
+# option 42 ip 192.168.1.1
+# domain-name test.local
+# lease 8
+
+# pconnection.yml
+
+# ---
+# vars:
+# dhcp_pool:
+# name: "{{ item.name }}"
+# network: "{{ item.network_ip }}"
+# subnet: "{{ item.network_subnet }}"
+# dns_servers: "{{ item.dns_servers_1 }}{{ item.dns_servers_2 }}"
+# domain_name: "{{ item.domain_name_0 }}{{ item.domain_name_1 }}{{ item.domain_name_2 }}{{ item.domain_name_3 }}"
+# options: "{{ item.options_1 }}{{ item.options_2 }}"
+# lease_days: "{{ item.lease_days }}"
+# lease_hours: "{{ item.lease_hours }}"
+# lease_minutes: "{{ item.lease_minutes }}"
+
+# keys:
+# dhcp_pools:
+# value: "{{ dhcp_pool }}"
+# items: "^ip dhcp pool (
+# ?P[^\\n]+)\\s+(?:import (?Pall)\\s*)?(?:network (?P[\\d.]+)
+# (?P[\\d.]+)?\\s*)?(?:update dns\\s*)?(?:host (?P[\\d.]+)
+# (?P[\\d.]+)\\s*)?(?:domain-name (?P[\\w._-]+)\\s+)?
+# (?:default-router (?P[\\d.]+)\\s*)?(?:dns-server
+# (?P(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name (?P[\\w._-]+)\\s+)?
+# (?P(?:option [^\\n]+\\n*\\s*)*)?(?:domain-name (?P[\\w._-]+)\\s+)?(?P(?:option [^\\n]+\\n*\\s*)*)?
+# (?:dns-server (?P(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name
+# (?P[\\w._-]+)\\s*)?(lease (?P\\d+)(?: (?P\\d+))?(?: (?P\\d+))?\\s*)?(?:update arp)?"
+
+# playbook
+
+- name: Add config data
+ ansible.builtin.set_fact:
+ opconfig: "{{lookup('ansible.builtin.file', 'outputConfig') }}"
+
+- name: Parse Data
+ ansible.builtin.set_fact:
+ output: "{{ opconfig | parse_cli('pconnection.yml') }}"
+
+
+# Task Output
+# -----------
+#
+# TASK [Add config data]
+# ok: [host] => changed=false
+# ansible_facts:
+# xml: |-
+# ip dhcp pool Data
+# import all
+# network 192.168.1.0 255.255.255.0
+# update dns
+# default-router 192.168.1.1
+# dns-server 192.168.1.1 8.8.8.8
+# option 42 ip 192.168.1.1
+# domain-name test.local
+# lease 8
+
+# TASK [Parse Data]
+# ok: [host] => changed=false
+# ansible_facts:
+# output:
+# dhcp_pools:
+# - dns_servers: 192.168.1.1 8.8.8.8
+# domain_name: test.local
+# lease_days: 8
+# lease_hours: null
+# lease_minutes: null
+# name: Data
+# network: 192.168.1.0
+# options: |-
+# option 42 ip 192.168.1.1
+# subnet: 255.255.255.0
+"""
+
+from ansible.errors import AnsibleFilterError
+from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
+ AnsibleArgSpecValidator,
+)
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.parse_cli import parse_cli
+
+
+try:
+ from jinja2.filters import pass_environment
+except ImportError:
+ from jinja2.filters import environmentfilter as pass_environment
+
+
+@pass_environment
+def _parse_cli(*args, **kwargs):
+ """Extend vlan data"""
+
+ keys = ["output", "tmpl"]
+ data = dict(zip(keys, args[1:]))
+ data.update(kwargs)
+ aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="parse_cli")
+ valid, errors, updated_data = aav.validate()
+ if not valid:
+ raise AnsibleFilterError(errors)
+ return parse_cli(**updated_data)
+
+
+class FilterModule(object):
+ """parse_cli"""
+
+ def filters(self):
+ """a mapping of filter names to functions"""
+ return {"parse_cli": _parse_cli}
diff --git a/plugins/filter/parse_cli_textfsm.py b/plugins/filter/parse_cli_textfsm.py
new file mode 100644
index 000000000..8b7a9612f
--- /dev/null
+++ b/plugins/filter/parse_cli_textfsm.py
@@ -0,0 +1,121 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The parse_cli_textfsm filter plugin
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+name: parse_cli_textfsm
+author: Peter Sprygada (@privateip)
+version_added: "1.0.0"
+short_description: parse_cli_textfsm filter plugin.
+description:
+ - The network filters also support parsing the output of a CLI command using the TextFSM library.
+ To parse the CLI output with TextFSM use this filter.
+ - Using the parameters below - C(data | ansible.netcommon.parse_cli_textfsm(template.yml))
+notes:
+ - Use of the TextFSM filter requires the TextFSM library to be installed.
+options:
+ value:
+ description:
+ - This source data on which parse_cli_textfsm invokes.
+ type: raw
+ required: True
+ template:
+ description:
+ - The template to compare it with.
+ - For example C(data | ansible.netcommon.parse_cli_textfsm(template.yml)),
+ in this case C(data) represents this option.
+ type: str
+"""
+
+EXAMPLES = r"""
+# Using parse_cli_textfsm
+
+- name: "Fetch command output"
+ cisco.ios.ios_command:
+ commands:
+ - show lldp neighbors
+ register: lldp_output
+
+- name: "Invoke parse_cli_textfsm"
+ ansible.builtin.set_fact:
+ device_neighbors: "{{ lldp_output.stdout[0] | parse_cli_textfsm('~/ntc-templates/templates/cisco_ios_show_lldp_neighbors.textfsm') }}"
+
+- name: "Debug"
+ ansible.builtindebug:
+ msg: "{{ device_neighbors }}"
+
+# Task Output
+# -----------
+#
+# TASK [Fetch command output]
+# ok: [rtr-2]
+
+# TASK [Invoke parse_cli_textfsm]
+# ok: [rtr-1]
+
+# TASK [Debug]
+# ok: [rtr-1] => {
+# "msg": [
+# {
+# "CAPABILITIES": "R",
+# "LOCAL_INTERFACE": "Gi0/0",
+# "NEIGHBOR": "rtr-3",
+# "NEIGHBOR_INTERFACE": "Gi0/0"
+# },
+# {
+# "CAPABILITIES": "R",
+# "LOCAL_INTERFACE": "Gi0/1",
+# "NEIGHBOR": "rtr-1",
+# "NEIGHBOR_INTERFACE": "Gi0/1"
+# }
+# ]
+# }
+"""
+
+from ansible.errors import AnsibleFilterError
+from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
+ AnsibleArgSpecValidator,
+)
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.parse_cli_textfsm import (
+ parse_cli_textfsm,
+)
+
+
+try:
+ from jinja2.filters import pass_environment
+except ImportError:
+ from jinja2.filters import environmentfilter as pass_environment
+
+
+@pass_environment
+def _parse_cli_textfsm(*args, **kwargs):
+ """Extend vlan data"""
+
+ keys = ["value", "template"]
+ data = dict(zip(keys, args[1:]))
+ data.update(kwargs)
+ aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="parse_cli_textfsm")
+ valid, errors, updated_data = aav.validate()
+ if not valid:
+ raise AnsibleFilterError(errors)
+ return parse_cli_textfsm(**updated_data)
+
+
+class FilterModule(object):
+ """parse_cli_textfsm"""
+
+ def filters(self):
+ """a mapping of filter names to functions"""
+ return {"parse_cli_textfsm": _parse_cli_textfsm}
diff --git a/plugins/filter/parse_xml.py b/plugins/filter/parse_xml.py
new file mode 100644
index 000000000..78e45cfe8
--- /dev/null
+++ b/plugins/filter/parse_xml.py
@@ -0,0 +1,195 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The parse_xml filter plugin
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+name: parse_xml
+author: Ganesh Nalawade (@ganeshrn)
+version_added: "1.0.0"
+short_description: The parse_xml filter plugin.
+description:
+ - This filter will load the spec file and pass the command output
+ through it, returning JSON output.
+ - The YAML spec file defines how to parse the CLI output.
+notes:
+ - To convert the XML output of a network device command into structured JSON output.
+options:
+ output:
+ description:
+ - This source xml on which parse_xml invokes.
+ type: raw
+ required: True
+ tmpl:
+ description:
+ - The spec file should be valid formatted YAML.
+ It defines how to parse the XML output and return JSON data.
+ - For example C(xml_data | ansible.netcommon.parse_xml(template.yml)),
+ in this case C(xml_data) represents xml data option.
+ type: string
+"""
+
+EXAMPLES = r"""
+# Using parse_xml
+
+# example_output.xml
+
+#
+#
+#
+#
+#
+#
+# 0/0/CPU0
+#
+# true
+# ntp-leap-no-warning
+#
+#
+# ntp-mode-client
+# true
+# 10.1.1.1
+# 0
+#
+# -1
+#
+#
+#
+# ntp-mode-client
+# true
+# 172.16.252.29
+# 255
+#
+# 991
+#
+#
+#
+#
+#
+#
+#
+
+# parse_xml.yml
+
+# ---
+# vars:
+# ntp_peers:
+# address: "{{ item.address }}"
+# reachability: "{{ item.reachability}}"
+# keys:
+# result:
+# value: "{{ ntp_peers }}"
+# top: data/ntp/nodes/node/associations
+# items:
+# address: peer-summary-info/peer-info-common/address
+# reachability: peer-summary-info/peer-info-common/reachability
+
+
+- name: Facts setup
+ ansible.builtin.set_fact:
+ xml: "{{ lookup('file', 'example_output.xml') }}"
+
+- name: Parse xml invocation
+ ansible.builtin.debug:
+ msg: "{{ xml | ansible.netcommon.parse_xml('parse_xml.yml') }}"
+
+
+# Task Output
+# -----------
+#
+# TASK [set xml Data]
+# ok: [host] => changed=false
+# ansible_facts:
+# xml: |-
+#
+#
+#
+#
+#
+#
+# 0/0/CPU0
+#
+# true
+# ntp-leap-no-warning
+#
+#
+# ntp-mode-client
+# true
+# 10.1.1.1
+# 0
+#
+# -1
+#
+#
+#
+# ntp-mode-client
+# true
+# 172.16.252.29
+# 255
+#
+# 991
+#
+#
+#
+#
+#
+#
+#
+
+# TASK [Parse Data]
+# ok: [host] => changed=false
+# ansible_facts:
+# output:
+# result:
+# - address:
+# - 10.1.1.1
+# - 172.16.252.29
+# reachability:
+# - '0'
+# - '255'
+"""
+
+from ansible.errors import AnsibleFilterError
+from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
+ AnsibleArgSpecValidator,
+)
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.parse_xml import parse_xml
+
+
+try:
+ from jinja2.filters import pass_environment
+except ImportError:
+ from jinja2.filters import environmentfilter as pass_environment
+
+
+@pass_environment
+def _parse_xml(*args, **kwargs):
+ """Extend vlan data"""
+
+ keys = ["output", "tmpl"]
+ data = dict(zip(keys, args[1:]))
+ data.update(kwargs)
+ aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="parse_xml")
+ valid, errors, updated_data = aav.validate()
+ if not valid:
+ raise AnsibleFilterError(errors)
+ return parse_xml(**updated_data)
+
+
+class FilterModule(object):
+ """parse_xml"""
+
+ def filters(self):
+ """a mapping of filter names to functions"""
+ return {"parse_xml": _parse_xml}
diff --git a/plugins/filter/type5_pw.py b/plugins/filter/type5_pw.py
new file mode 100644
index 000000000..a053d82ea
--- /dev/null
+++ b/plugins/filter/type5_pw.py
@@ -0,0 +1,96 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The type5_pw filter plugin
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+name: type5_pw
+author: Ken Celenza (@itdependsnetworks)
+version_added: "1.0.0"
+short_description: The type5_pw filter plugin.
+description:
+ - Filter plugin to produce cisco type5 hashed password.
+ - Using the parameters below - C(xml_data | ansible.netcommon.type5_pw(template.yml))
+notes:
+ - The filter plugin generates cisco type5 hashed password.
+options:
+ password:
+ description:
+ - The password to be hashed.
+ type: str
+ required: True
+ salt:
+ description:
+ - Mention the salt to hash the password.
+ type: str
+"""
+
+EXAMPLES = r"""
+# Using type5_pw
+
+- name: Set some facts
+ ansible.builtin.set_fact:
+ password: "cisco@123"
+
+- name: Filter type5_pw invocation
+ ansible.builtin.debug:
+ msg: "{{ password | ansible.netcommon.type5_pw(salt='avs') }}"
+
+
+# Task Output
+# -----------
+#
+# TASK [Set some facts]
+# ok: [host] => changed=false
+# ansible_facts:
+# password: cisco@123
+
+# TASK [Filter type5_pw invocation]
+# ok: [host] =>
+# msg: $1$avs$uSTOEMh65qzvpb9yBMpzd/
+"""
+
+from ansible.errors import AnsibleFilterError
+from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
+ AnsibleArgSpecValidator,
+)
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.type5_pw import type5_pw
+
+
+try:
+ from jinja2.filters import pass_environment
+except ImportError:
+ from jinja2.filters import environmentfilter as pass_environment
+
+
+@pass_environment
+def _type5_pw(*args, **kwargs):
+ """Extend vlan data"""
+
+ keys = ["password", "salt"]
+ data = dict(zip(keys, args[1:]))
+ data.update(kwargs)
+ aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="type5_pw")
+ valid, errors, updated_data = aav.validate()
+ if not valid:
+ raise AnsibleFilterError(errors)
+ return type5_pw(**updated_data)
+
+
+class FilterModule(object):
+ """type5_pw"""
+
+ def filters(self):
+ """a mapping of filter names to functions"""
+ return {"type5_pw": _type5_pw}
diff --git a/plugins/filter/vlan_expander.py b/plugins/filter/vlan_expander.py
new file mode 100644
index 000000000..b279564eb
--- /dev/null
+++ b/plugins/filter/vlan_expander.py
@@ -0,0 +1,101 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The vlan_expander filter plugin
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+name: vlan_expander
+author: Akira Yokochi (@akira6592)
+version_added: "2.3.0"
+short_description: The vlan_expander filter plugin.
+description:
+ - Expand shorthand list of VLANs to list all VLANs. Inverse of vlan_parser
+ - Using the parameters below - C(vlans_data | ansible.netcommon.vlan_expander)
+notes:
+ - The filter plugin extends vlans when data provided in range or comma separated.
+options:
+ data:
+ description:
+ - This option represents a string containing the range of vlans.
+ type: string
+ required: True
+"""
+
+EXAMPLES = r"""
+# Using vlan_expander
+
+- name: Setting host facts for vlan_expander filter plugin
+ ansible.builtin.set_fact:
+ vlan_ranges: "1,10-12,15,20-22"
+
+- name: Invoke vlan_expander filter plugin
+ ansible.builtin.set_fact:
+ extended_vlans: "{{ vlan_ranges | ansible.netcommon.vlan_expander }}"
+
+
+# Task Output
+# -----------
+#
+# TASK [Setting host facts for vlan_expander filter plugin]
+# ok: [host] => changed=false
+# ansible_facts:
+# vlan_ranges: 1,10-12,15,20-22
+
+# TASK [Invoke vlan_expander filter plugin]
+# ok: [host] => changed=false
+# ansible_facts:
+# extended_vlans:
+# - 1
+# - 10
+# - 11
+# - 12
+# - 15
+# - 20
+# - 21
+# - 22
+"""
+
+from ansible.errors import AnsibleFilterError
+from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
+ AnsibleArgSpecValidator,
+)
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.vlan_expander import vlan_expander
+
+
+try:
+ from jinja2.filters import pass_environment
+except ImportError:
+ from jinja2.filters import environmentfilter as pass_environment
+
+
+@pass_environment
+def _vlan_expander(*args, **kwargs):
+ """Extend vlan data"""
+
+ keys = ["data"]
+ data = dict(zip(keys, args[1:]))
+ data.update(kwargs)
+ aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="vlan_expander")
+ valid, errors, updated_data = aav.validate()
+ if not valid:
+ raise AnsibleFilterError(errors)
+ return vlan_expander(**updated_data)
+
+
+class FilterModule(object):
+ """vlan_expander"""
+
+ def filters(self):
+ """a mapping of filter names to functions"""
+ return {"vlan_expander": _vlan_expander}
diff --git a/plugins/filter/vlan_parser.py b/plugins/filter/vlan_parser.py
new file mode 100644
index 000000000..db549ae78
--- /dev/null
+++ b/plugins/filter/vlan_parser.py
@@ -0,0 +1,137 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The vlan_parser filter plugin
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+name: vlan_parser
+author: Steve Dodd (@idahood)
+version_added: "1.0.0"
+short_description: The vlan_parser filter plugin.
+description:
+ - The filter plugin converts a list of vlans to IOS like vlan configuration.
+ - Converts list to a list of range of numbers into multiple lists.
+ - C(vlans_data | ansible.netcommon.vlan_parser(first_line_len = 20, other_line_len=20))
+notes:
+ - The filter plugin extends vlans when data provided in range or comma separated.
+options:
+ data:
+ description:
+ - This option represents a list containing vlans.
+ type: list
+ required: True
+ first_line_len:
+ description:
+ - The first line of the list can be first_line_len characters long.
+ type: int
+ default: 48
+ other_line_len:
+ description:
+ - The subsequent list lines can be other_line_len characters.
+ type: int
+ default: 44
+"""
+
+EXAMPLES = r"""
+# Using vlan_parser
+
+- name: Setting host facts for vlan_parser filter plugin
+ ansible.builtin.set_fact:
+ vlans:
+ [
+ 100,
+ 1688,
+ 3002,
+ 3003,
+ 3004,
+ 3005,
+ 3102,
+ 3103,
+ 3104,
+ 3105,
+ 3802,
+ 3900,
+ 3998,
+ 3999,
+ ]
+
+- name: Invoke vlan_parser filter plugin
+ ansible.builtin.set_fact:
+ vlans_ranges: "{{ vlans | ansible.netcommon.vlan_parser(first_line_len = 20, other_line_len=20) }}"
+
+
+# Task Output
+# -----------
+#
+# TASK [Setting host facts for vlan_parser filter plugin]
+# ok: [host] => changed=false
+# ansible_facts:
+# vlans:
+# - 100
+# - 1688
+# - 3002
+# - 3003
+# - 3004
+# - 3005
+# - 3102
+# - 3103
+# - 3104
+# - 3105
+# - 3802
+# - 3900
+# - 3998
+# - 3999
+
+# TASK [Invoke vlan_parser filter plugin]
+# ok: [host] => changed=false
+# ansible_facts:
+# msg:
+# - 100,1688,3002-3005
+# - 3102-3105,3802,3900
+# - 3998,3999
+"""
+
+from ansible.errors import AnsibleFilterError
+from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import (
+ AnsibleArgSpecValidator,
+)
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.vlan_parser import vlan_parser
+
+
+try:
+ from jinja2.filters import pass_environment
+except ImportError:
+ from jinja2.filters import environmentfilter as pass_environment
+
+
+@pass_environment
+def _vlan_parser(*args, **kwargs):
+ """Extend vlan data"""
+
+ keys = ["data", "first_line_len", "other_line_len"]
+ data = dict(zip(keys, args[1:]))
+ data.update(kwargs)
+ aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="vlan_parser")
+ valid, errors, updated_data = aav.validate()
+ if not valid:
+ raise AnsibleFilterError(errors)
+ return vlan_parser(**updated_data)
+
+
+class FilterModule(object):
+ """vlan_parser"""
+
+ def filters(self):
+ """a mapping of filter names to functions"""
+ return {"vlan_parser": _vlan_parser}
diff --git a/plugins/plugin_utils/comp_type5.py b/plugins/plugin_utils/comp_type5.py
new file mode 100644
index 000000000..9672e0ebc
--- /dev/null
+++ b/plugins/plugin_utils/comp_type5.py
@@ -0,0 +1,27 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The comp_type5 plugin code
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.hash_salt import hash_salt
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.type5_pw import type5_pw
+
+
+def comp_type5(unencrypted_password, encrypted_password, return_original=False):
+ salt = hash_salt(encrypted_password)
+ if type5_pw(unencrypted_password, salt) == encrypted_password:
+ if return_original is True:
+ return encrypted_password
+ else:
+ return True
+ return False
diff --git a/plugins/plugin_utils/hash_salt.py b/plugins/plugin_utils/hash_salt.py
new file mode 100644
index 000000000..e49d40164
--- /dev/null
+++ b/plugins/plugin_utils/hash_salt.py
@@ -0,0 +1,28 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The hash_salt plugin code
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from ansible.errors import AnsibleFilterError
+
+
+def _raise_error(msg):
+ raise AnsibleFilterError(msg)
+
+
+def hash_salt(password):
+ split_password = password.split("$")
+ if len(split_password) != 4:
+ _raise_error("Could not parse salt out password correctly from {0}".format(password))
+ else:
+ return split_password[2]
diff --git a/plugins/plugin_utils/parse_cli.py b/plugins/plugin_utils/parse_cli.py
new file mode 100644
index 000000000..c99d5baad
--- /dev/null
+++ b/plugins/plugin_utils/parse_cli.py
@@ -0,0 +1,220 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The parse_xml plugin code
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import os
+import re
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common._collections_compat import Mapping
+from ansible.module_utils.six import iteritems, string_types
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import Template
+
+
+try:
+ import yaml
+
+ HAS_YAML = True
+except ImportError:
+ HAS_YAML = False
+
+
+def _raise_error(msg):
+ raise AnsibleFilterError(msg)
+
+
+def re_matchall(regex, value):
+ objects = list()
+ for match in re.findall(regex.pattern, value, re.M):
+ obj = {}
+ if regex.groupindex:
+ for name, index in iteritems(regex.groupindex):
+ if len(regex.groupindex) == 1:
+ obj[name] = match
+ else:
+ obj[name] = match[index - 1]
+ objects.append(obj)
+ return objects
+
+
+def re_search(regex, value):
+ obj = {}
+ match = regex.search(value, re.M)
+ if match:
+ items = list(match.groups())
+ if regex.groupindex:
+ for name, index in iteritems(regex.groupindex):
+ obj[name] = items[index - 1]
+ return obj
+
+
+def re_finditer(regex, value):
+ iter_obj = re.finditer(regex, value)
+ values = None
+ for each in iter_obj:
+ if not values:
+ values = each.groupdict()
+ else:
+ # for backward compatibility
+ values.update(each.groupdict())
+ # for backward compatibility
+ values["match"] = list(each.groups())
+ groups = each.groupdict()
+ for group in groups:
+ if not values.get("match_all"):
+ values["match_all"] = dict()
+ if not values["match_all"].get(group):
+ values["match_all"][group] = list()
+ values["match_all"][group].append(groups[group])
+ return values
+
+
+def parse_cli(output, tmpl):
+ if not isinstance(output, string_types):
+ _raise_error(
+ "parse_cli input should be a string, but was given a input of %s" % (type(output))
+ )
+
+ if not os.path.exists(tmpl):
+ _raise_error("unable to locate parse_cli template: %s" % tmpl)
+
+ try:
+ template = Template()
+ except ImportError as exc:
+ _raise_error(to_native(exc))
+
+ with open(tmpl) as tmpl_fh:
+ tmpl_content = tmpl_fh.read()
+
+ spec = yaml.safe_load(tmpl_content)
+ obj = {}
+
+ for name, attrs in iteritems(spec["keys"]):
+ value = attrs["value"]
+
+ try:
+ variables = spec.get("vars", {})
+ value = template(value, variables)
+ except Exception:
+ pass
+
+ if "start_block" in attrs and "end_block" in attrs:
+ start_block = re.compile(attrs["start_block"])
+ end_block = re.compile(attrs["end_block"])
+
+ blocks = list()
+ lines = None
+ block_started = False
+
+ for line in output.split("\n"):
+ match_start = start_block.match(line)
+ match_end = end_block.match(line)
+
+ if match_start:
+ lines = list()
+ lines.append(line)
+ block_started = True
+
+ elif match_end:
+ if lines:
+ lines.append(line)
+ blocks.append("\n".join(lines))
+ lines = None
+ block_started = False
+
+ elif block_started:
+ if lines:
+ lines.append(line)
+
+ regex_items = [re.compile(r) for r in attrs["items"]]
+ objects = list()
+
+ for block in blocks:
+ if isinstance(value, Mapping) and "key" not in value:
+ items = list()
+ for regex in regex_items:
+ items.append(re_finditer(regex, block))
+
+ obj = {}
+ for k, v in iteritems(value):
+ try:
+ obj[k] = template(v, {"item": items}, fail_on_undefined=False)
+ except Exception:
+ obj[k] = None
+ objects.append(obj)
+
+ elif isinstance(value, Mapping):
+ items = list()
+ for regex in regex_items:
+ items.append(re_finditer(regex, block))
+
+ key = template(value["key"], {"item": items})
+ values = dict(
+ [(k, template(v, {"item": items})) for k, v in iteritems(value["values"])]
+ )
+ objects.append({key: values})
+
+ return objects
+
+ elif "items" in attrs:
+ regexp = re.compile(attrs["items"])
+ when = attrs.get("when")
+ conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" % when
+
+ if isinstance(value, Mapping) and "key" not in value:
+ values = list()
+
+ for item in re_matchall(regexp, output):
+ entry = {}
+
+ for item_key, item_value in iteritems(value):
+ entry[item_key] = template(item_value, {"item": item})
+
+ if when:
+ if template(conditional, {"item": entry}):
+ values.append(entry)
+ else:
+ values.append(entry)
+
+ obj[name] = values
+
+ elif isinstance(value, Mapping):
+ values = dict()
+
+ for item in re_matchall(regexp, output):
+ entry = {}
+
+ for item_key, item_value in iteritems(value["values"]):
+ entry[item_key] = template(item_value, {"item": item})
+
+ key = template(value["key"], {"item": item})
+
+ if when:
+ if template(conditional, {"item": {"key": key, "value": entry}}):
+ values[key] = entry
+ else:
+ values[key] = entry
+
+ obj[name] = values
+
+ else:
+ item = re_search(regexp, output)
+ obj[name] = template(value, {"item": item})
+
+ else:
+ obj[name] = value
+
+ return obj
diff --git a/plugins/plugin_utils/parse_cli_textfsm.py b/plugins/plugin_utils/parse_cli_textfsm.py
new file mode 100644
index 000000000..447a7cdd3
--- /dev/null
+++ b/plugins/plugin_utils/parse_cli_textfsm.py
@@ -0,0 +1,60 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The parse_cli_textfsm plugin code
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import os
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils._text import to_native
+from ansible.module_utils.six import string_types
+
+
+try:
+ import textfsm
+
+ HAS_TEXTFSM = True
+except ImportError:
+ HAS_TEXTFSM = False
+
+
+def _raise_error(msg):
+ raise AnsibleFilterError(msg)
+
+
+def parse_cli_textfsm(value, template):
+ if not HAS_TEXTFSM:
+ _raise_error("parse_cli_textfsm filter requires TextFSM library to be installed")
+
+ if not isinstance(value, string_types):
+ _raise_error(
+ "parse_cli_textfsm input should be a string, but was given a input of %s"
+ % (type(value))
+ )
+
+ if not os.path.exists(template):
+ _raise_error("unable to locate parse_cli_textfsm template: %s" % template)
+
+ try:
+ template = open(template)
+ except IOError as exc:
+ _raise_error(to_native(exc))
+
+ re_table = textfsm.TextFSM(template)
+ fsm_results = re_table.ParseText(value)
+
+ results = list()
+ for item in fsm_results:
+ results.append(dict(zip(re_table.header, item)))
+
+ return results
diff --git a/plugins/plugin_utils/parse_xml.py b/plugins/plugin_utils/parse_xml.py
new file mode 100644
index 000000000..55eeb6396
--- /dev/null
+++ b/plugins/plugin_utils/parse_xml.py
@@ -0,0 +1,151 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The parse_xml plugin code
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import os
+import traceback
+
+from xml.etree.ElementTree import fromstring
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common._collections_compat import Mapping
+from ansible.module_utils.six import iteritems, string_types
+from ansible.utils.display import Display
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import Template
+
+
+try:
+ import yaml
+
+ HAS_YAML = True
+except ImportError:
+ HAS_YAML = False
+
+display = Display()
+
+
+def _raise_error(msg):
+ raise AnsibleFilterError(msg)
+
+
+def _extract_param(template, root, attrs, value):
+ key = None
+ when = attrs.get("when")
+ conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" % when
+ param_to_xpath_map = attrs["items"]
+
+ if isinstance(value, Mapping):
+ key = value.get("key", None)
+ if key:
+ value = value["values"]
+
+ entries = dict() if key else list()
+
+ for element in root.findall(attrs["top"]):
+ entry = dict()
+ item_dict = dict()
+ for param, param_xpath in iteritems(param_to_xpath_map):
+ fields = None
+ try:
+ fields = element.findall(param_xpath)
+ except Exception:
+ display.warning(
+ "Failed to evaluate value of '%s' with XPath '%s'.\nUnexpected error: %s."
+ % (param, param_xpath, traceback.format_exc())
+ )
+
+ tags = param_xpath.split("/")
+
+ # check if xpath ends with attribute.
+ # If yes set attribute key/value dict to param value in case attribute matches
+ # else if it is a normal xpath assign matched element text value.
+ if len(tags) and tags[-1].endswith("]"):
+ if fields:
+ if len(fields) > 1:
+ item_dict[param] = [field.attrib for field in fields]
+ else:
+ item_dict[param] = fields[0].attrib
+ else:
+ item_dict[param] = {}
+ else:
+ if fields:
+ if len(fields) > 1:
+ item_dict[param] = [field.text for field in fields]
+ else:
+ item_dict[param] = fields[0].text
+ else:
+ item_dict[param] = None
+
+ if isinstance(value, Mapping):
+ for item_key, item_value in iteritems(value):
+ entry[item_key] = template(item_value, {"item": item_dict})
+ else:
+ entry = template(value, {"item": item_dict})
+
+ if key:
+ expanded_key = template(key, {"item": item_dict})
+ if when:
+ if template(
+ conditional,
+ {"item": {"key": expanded_key, "value": entry}},
+ ):
+ entries[expanded_key] = entry
+ else:
+ entries[expanded_key] = entry
+ else:
+ if when:
+ if template(conditional, {"item": entry}):
+ entries.append(entry)
+ else:
+ entries.append(entry)
+
+ return entries
+
+
+def parse_xml(output, tmpl):
+ if not os.path.exists(tmpl):
+ _raise_error("unable to locate parse_xml template: %s" % tmpl)
+
+ if not isinstance(output, string_types):
+ _raise_error("parse_xml works on string input, but given input of : %s" % type(output))
+
+ root = fromstring(output)
+ try:
+ template = Template()
+ except ImportError as exc:
+ raise AnsibleFilterError(to_native(exc))
+
+ with open(tmpl) as tmpl_fh:
+ tmpl_content = tmpl_fh.read()
+
+ spec = yaml.safe_load(tmpl_content)
+ obj = {}
+
+ for name, attrs in iteritems(spec["keys"]):
+ value = attrs["value"]
+
+ try:
+ variables = spec.get("vars", {})
+ value = template(value, variables)
+ except Exception:
+ pass
+
+ if "items" in attrs:
+ obj[name] = _extract_param(template, root, attrs, value)
+ else:
+ obj[name] = value
+
+ return obj
diff --git a/plugins/plugin_utils/type5_pw.py b/plugins/plugin_utils/type5_pw.py
new file mode 100644
index 000000000..279f3fcee
--- /dev/null
+++ b/plugins/plugin_utils/type5_pw.py
@@ -0,0 +1,48 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The type5_pw plugin code
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import string
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils._text import to_text
+from ansible.module_utils.six import string_types
+from ansible.utils.encrypt import passlib_or_crypt, random_password
+
+
+def _raise_error(msg):
+ raise AnsibleFilterError(msg)
+
+
+def type5_pw(password, salt=None):
+ if not isinstance(password, string_types):
+ _raise_error(
+ "type5_pw password input should be a string, but was given a input of %s"
+ % (type(password).__name__)
+ )
+
+ salt_chars = "".join((to_text(string.ascii_letters), to_text(string.digits), "./"))
+ if salt is not None and not isinstance(salt, string_types):
+ _raise_error(
+ "type5_pw salt input should be a string, but was given a input of %s"
+ % (type(salt).__name__)
+ )
+ elif not salt:
+ salt = random_password(length=4, chars=salt_chars)
+ elif not set(salt) <= set(salt_chars):
+ _raise_error("type5_pw salt used inproper characters, must be one of %s" % (salt_chars))
+
+ encrypted_password = passlib_or_crypt(password, "md5_crypt", salt=salt)
+
+ return encrypted_password
diff --git a/plugins/plugin_utils/vlan_expander.py b/plugins/plugin_utils/vlan_expander.py
new file mode 100644
index 000000000..77679928b
--- /dev/null
+++ b/plugins/plugin_utils/vlan_expander.py
@@ -0,0 +1,25 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The vlan_expander plugin code
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+
+def vlan_expander(data):
+ expanded_list = []
+ for each in data.split(","):
+ if "-" in each:
+ f, t = map(int, each.split("-"))
+ expanded_list.extend(range(f, t + 1))
+ else:
+ expanded_list.append(int(each))
+ return sorted(expanded_list)
diff --git a/plugins/plugin_utils/vlan_parser.py b/plugins/plugin_utils/vlan_parser.py
new file mode 100644
index 000000000..1fcef5436
--- /dev/null
+++ b/plugins/plugin_utils/vlan_parser.py
@@ -0,0 +1,93 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+"""
+The vlan_parser plugin code
+"""
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from ansible.errors import AnsibleFilterError
+
+
+def _raise_error(msg):
+ raise AnsibleFilterError(msg)
+
+
+def vlan_parser(data, first_line_len=48, other_line_len=44):
+ """
+ Input: Unsorted list of vlan integers
+ Output: Sorted string list of integers according to IOS-like vlan list rules
+
+ 1. Vlans are listed in ascending order
+ 2. Runs of 3 or more consecutive vlans are listed with a dash
+ 3. The first line of the list can be first_line_len characters long
+ 4. Subsequent list lines can be other_line_len characters
+ """
+ if not isinstance(data, (list)):
+ _raise_error("Input is not valid for vlan_parser")
+ # Sort and remove duplicates
+ sorted_list = sorted(set(data))
+
+ if sorted_list[0] < 1 or sorted_list[-1] > 4094:
+ _raise_error("Valid VLAN range is 1-4094")
+
+ parse_list = []
+ idx = 0
+ while idx < len(sorted_list):
+ start = idx
+ end = start
+ while end < len(sorted_list) - 1:
+ if sorted_list[end + 1] - sorted_list[end] == 1:
+ end += 1
+ else:
+ break
+
+ if start == end:
+ # Single VLAN
+ parse_list.append(str(sorted_list[idx]))
+ elif start + 1 == end:
+ # Run of 2 VLANs
+ parse_list.append(str(sorted_list[start]))
+ parse_list.append(str(sorted_list[end]))
+ else:
+ # Run of 3 or more VLANs
+ parse_list.append(str(sorted_list[start]) + "-" + str(sorted_list[end]))
+ idx = end + 1
+
+ line_count = 0
+ result = [""]
+ for vlans in parse_list:
+ # First line (" switchport trunk allowed vlan ")
+ if line_count == 0:
+ if len(result[line_count] + vlans) > first_line_len:
+ result.append("")
+ line_count += 1
+ result[line_count] += vlans + ","
+ else:
+ result[line_count] += vlans + ","
+
+ # Subsequent lines (" switchport trunk allowed vlan add ")
+ else:
+ if len(result[line_count] + vlans) > other_line_len:
+ result.append("")
+ line_count += 1
+ result[line_count] += vlans + ","
+ else:
+ result[line_count] += vlans + ","
+
+ # Remove trailing orphan commas
+ for idx in range(0, len(result)):
+ result[idx] = result[idx].rstrip(",")
+
+ # Sometimes text wraps to next line, but there are no remaining VLANs
+ if "" in result:
+ result.remove("")
+
+ return result
diff --git a/tests/unit/plugins/filter/comp_type5.py b/tests/unit/plugins/filter/comp_type5.py
new file mode 100644
index 000000000..9bcc0ba35
--- /dev/null
+++ b/tests/unit/plugins/filter/comp_type5.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import unittest
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.comp_type5 import comp_type5
+
+
+class TestComp_type5(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def test_comp_type5_plugin_1(self):
+ unencrypted_password = "cisco@123"
+ encrypted_password = "$1$avs$uSTOEMh65qzvpb9yBMpzd/"
+ args = [unencrypted_password, encrypted_password, False]
+ result = comp_type5(*args)
+ self.assertEqual(
+ True,
+ result,
+ )
+
+ def test_comp_type5_plugin_2(self):
+ unencrypted_password = "cisco@123"
+ encrypted_password = "$1$avs$uSTOEMh65qzvpb9yBMpzd/"
+ args = [unencrypted_password, encrypted_password, True]
+ result = comp_type5(*args)
+ self.assertEqual(
+ encrypted_password,
+ result,
+ )
diff --git a/tests/unit/plugins/filter/test_hash_salt.py b/tests/unit/plugins/filter/test_hash_salt.py
new file mode 100644
index 000000000..54ffbfb19
--- /dev/null
+++ b/tests/unit/plugins/filter/test_hash_salt.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import unittest
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.hash_salt import hash_salt
+
+
+class Testhash_salt(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def test_hash_salt_plugin_1(self):
+ password = "$1$avs$uSTOEMh65qzvpb9yBMpzd/TESTPASS"
+ args = [password[0:-8]]
+ result = hash_salt(*args)
+ self.assertEqual(
+ "avs",
+ result,
+ )
diff --git a/tests/unit/plugins/filter/test_network.py b/tests/unit/plugins/filter/test_network.py
index 1230800d7..a3b50ef83 100644
--- a/tests/unit/plugins/filter/test_network.py
+++ b/tests/unit/plugins/filter/test_network.py
@@ -23,14 +23,12 @@
import sys
import unittest
-from ansible_collections.ansible.netcommon.plugins.filter.network import (
- comp_type5,
- hash_salt,
- parse_xml,
- type5_pw,
- vlan_expander,
- vlan_parser,
-)
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.comp_type5 import comp_type5
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.hash_salt import hash_salt
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.parse_xml import parse_xml
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.type5_pw import type5_pw
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.vlan_expander import vlan_expander
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.vlan_parser import vlan_parser
fixture_path = os.path.join(os.path.dirname(__file__), "fixtures", "network")
@@ -303,6 +301,7 @@ def test_multi_ranges(self):
def test_no_ranges(self):
raw_list = "1,3,5"
expanded_list = [1, 3, 5]
+ print(vlan_expander(raw_list))
self.assertEqual(vlan_expander(raw_list), expanded_list)
diff --git a/tests/unit/plugins/filter/test_type5_pw.py b/tests/unit/plugins/filter/test_type5_pw.py
new file mode 100644
index 000000000..a1ea8c905
--- /dev/null
+++ b/tests/unit/plugins/filter/test_type5_pw.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import unittest
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.type5_pw import type5_pw
+
+
+class TestType5_pw(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def test_type5_pw_plugin_1(self):
+ password = "cisco"
+ salt = "nTc1"
+ args = [password, salt]
+ result = type5_pw(*args)
+ self.assertEqual(
+ "$1$nTc1$Z28sUTcWfXlvVe2x.3XAa.TESTPASS",
+ result + "TESTPASS",
+ )
+
+ def test_type5_pw_plugin_2(self):
+ password = "cisco"
+ args = [password]
+ result = type5_pw(*args)
+ self.assertEqual(
+ len(result),
+ 30,
+ )
diff --git a/tests/unit/plugins/filter/test_vlan_extender.py b/tests/unit/plugins/filter/test_vlan_extender.py
new file mode 100644
index 000000000..4537e1eed
--- /dev/null
+++ b/tests/unit/plugins/filter/test_vlan_extender.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import unittest
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.vlan_expander import vlan_expander
+
+
+class TestVlanExtender(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def test_vlan_extender_plugin_1(self):
+ data = "1,13-19,24,26,34-56"
+ args = [data]
+ result = vlan_expander(*args)
+ self.assertEqual(
+ result,
+ [
+ 1,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 24,
+ 26,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 42,
+ 43,
+ 44,
+ 45,
+ 46,
+ 47,
+ 48,
+ 49,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ ],
+ )
+
+ def test_vlan_extender_plugin_2(self):
+ data = "13-19"
+ args = [data]
+ result = vlan_expander(*args)
+ self.assertEqual(
+ result,
+ [
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ ],
+ )
diff --git a/tests/unit/plugins/filter/test_vlan_parser.py b/tests/unit/plugins/filter/test_vlan_parser.py
new file mode 100644
index 000000000..3765bdf28
--- /dev/null
+++ b/tests/unit/plugins/filter/test_vlan_parser.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# Copyright 2023 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import unittest
+
+from ansible.errors import AnsibleFilterError
+
+from ansible_collections.ansible.netcommon.plugins.plugin_utils.vlan_parser import vlan_parser
+
+
+class TestVlanParser(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def test_vlan_parser_plugin_1(self):
+ data = [
+ 1,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 24,
+ 26,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 42,
+ 43,
+ 44,
+ 45,
+ 46,
+ 47,
+ 48,
+ 49,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ ]
+ args = [data]
+ result = vlan_parser(*args)
+ self.assertEqual(
+ result[0],
+ "1,13-19,24,26,34-56",
+ )
+
+ def test_vlan_parser_plugin_2(self):
+ data = [1, 2, 3]
+ args = [data]
+ result = vlan_parser(*args)
+ self.assertEqual(
+ result[0],
+ "1-3",
+ )
+
+ def test_vlan_parser_fail_wrong_data(self):
+ data = "13"
+ args = [data]
+ with self.assertRaises(AnsibleFilterError) as error:
+ vlan_parser(*args)
+ self.assertIn(
+ "Input is not valid for vlan_parser",
+ str(error.exception),
+ )
+
+ def test_vlan_parser_fail_out_range(self):
+ data = [
+ 1,
+ 2013,
+ 2014,
+ 2015,
+ 2016,
+ 2017,
+ 2018,
+ 2019,
+ 2024,
+ 2026,
+ 4034,
+ 4035,
+ 4036,
+ 4037,
+ 4038,
+ 4039,
+ 4040,
+ 4041,
+ 4042,
+ 4311,
+ ]
+ args = [data]
+ with self.assertRaises(AnsibleFilterError) as error:
+ vlan_parser(*args)
+ self.assertIn(
+ "Valid VLAN range is 1-4094",
+ str(error.exception),
+ )