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 + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ encrypted_password + +
+ string + / required +
+
+ + +
The encrypted text.
+
+
+ return_original + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+ +
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 + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ 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 + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ 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 + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ 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 + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ 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 + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ 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 + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ 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 + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ 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), + )