diff --git a/.gitignore b/.gitignore index 6f8f41d..5101508 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ __pycache__/ .Python env/ bin/ -build/ +./build/ develop-eggs/ dist/ eggs/ @@ -63,4 +63,4 @@ tags .compiled -module_docs/ \ No newline at end of file +module_docs/ diff --git a/collection/LICENSE b/collection/LICENSE new file mode 100644 index 0000000..5c304d1 --- /dev/null +++ b/collection/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/collection/README.md b/collection/README.md new file mode 100644 index 0000000..8709ad6 --- /dev/null +++ b/collection/README.md @@ -0,0 +1,424 @@ +# napalm-ansible + +Collection of ansible modules that use [napalm](https://github.com/napalm-automation/napalm) to retrieve data or modify configuration on networking devices. + +Modules +======= + +The following modules are currently available: + +- ``napalm_cli`` +- ``napalm_diff_yang`` +- ``napalm_get_facts`` +- ``napalm_install_config`` +- ``napalm_parse_yang`` +- ``napalm_ping`` +- ``napalm_translate_yang`` +- ``napalm_validate`` + +Install +======= + +To install run: + +``` +pip install napalm +ansible-galaxy collection install napalm.napalm +``` + +Dependencies +======= +* [napalm](https://github.com/napalm-automation/napalm) 3.2.0 or later +* [ansible](https://github.com/ansible/ansible) 2.9.20 or later + + +Examples +======= + +### Cisco IOS + +#### Inventory (IOS) + +```INI +[cisco] +cisco5 ansible_host=cisco5.domain.com + +[cisco:vars] +# Must match Python that NAPALM is installed into. +ansible_python_interpreter=/path/to/venv/bin/python +ansible_network_os=ios +ansible_connection=network_cli +ansible_user=admin +ansible_ssh_pass=my_password +``` + +#### Playbook (IOS) + +```YAML +--- +- name: NAPALM get_facts and get_interfaces + hosts: cisco5 + gather_facts: False + tasks: + - name: napalm get_facts + napalm.napalm.get_facts: + filter: facts,interfaces + + - debug: + var: napalm_facts +``` + +#### Playbook Output (IOS) + +```INI +$ ansible-playbook napalm_get_ios.yml + +PLAY [NAPALM get_facts and get_interfaces] ********* + +TASK [napalm get_facts] **************************** +ok: [cisco5] + +TASK [debug] *************************************** +ok: [cisco5] => { + "napalm_facts": { + "fqdn": "cisco5.domain.com", + "hostname": "cisco5", + "interface_list": [ + "GigabitEthernet1", + "GigabitEthernet2", + "GigabitEthernet3", + "GigabitEthernet4", + "GigabitEthernet5", + "GigabitEthernet6", + "GigabitEthernet7" + ], + "model": "CSR1000V", + "os_version": "Virtual XE Software, Version 16.9.3, RELEASE SOFTWARE (fc2)", + "serial_number": "9700000000P", + "uptime": 13999500, + "vendor": "Cisco" + } +} + +PLAY RECAP ***************************************** +cisco5 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +``` + +### Arista EOS + +#### Inventory (EOS) + +```INI +[arista] +arista5 ansible_host=arista5.domain.com + +[arista:vars] +# Must match Python that NAPALM is installed into. +ansible_python_interpreter=/path/to/venv/bin/python +ansible_network_os=eos +# Continue using 'network_cli' (NAPALM module itself will use eAPI) +ansible_connection=network_cli +ansible_user=admin +ansible_ssh_pass=my_password +``` + +#### Playbook (EOS) + +```YAML +--- +- name: NAPALM get_facts and get_interfaces + hosts: arista5 + gather_facts: False + tasks: + - name: napalm get_facts + napalm.napalm.get_facts: + filter: facts,interfaces + + - debug: + var: napalm_facts +``` + +#### Playbook Output (EOS) + +```INI +$ ansible-playbook napalm_get_arista.yml + +PLAY [NAPALM get_facts and get_interfaces] ********* + +TASK [napalm get_facts] **************************** +ok: [arista5] + +TASK [debug] *************************************** +ok: [arista5] => { + "napalm_facts": { + "fqdn": "arista5", + "hostname": "arista5", + "interface_list": [ + "Ethernet1", + "Ethernet2", + "Ethernet3", + "Ethernet4", + "Ethernet5", + "Ethernet6", + "Ethernet7", + "Management1", + "Vlan1" + ], + "model": "vEOS", + "os_version": "4.20.10M-10040268.42010M", + "serial_number": "", + "uptime": 12858220, + "vendor": "Arista" + } +} + +PLAY RECAP **************************************** +arista5 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Cisco NX-OS + +#### Inventory (NX-OS) + +```INI +[nxos] +nxos1 ansible_host=nxos1.domain.com + +[nxos:vars] +# Must match Python that NAPALM is installed into. +ansible_python_interpreter=/path/to/venv/bin/python +ansible_network_os=nxos +# Continue using 'network_cli' (NAPALM module itself will use NX-API) +ansible_connection=network_cli +ansible_user=admin +ansible_ssh_pass=my_password +``` + +#### Playbook (NX-OS NX-API) + +```YAML +--- +- name: napalm + hosts: nxos1 + gather_facts: False + tasks: + - name: Retrieve get_facts, get_interfaces + napalm.napalm.get_facts: + filter: facts,interfaces + # Specify NX-API Port + optional_args: + port: 8443 + + - debug: + var: napalm_facts +``` + +#### Playbook Output (NX-OS NX-API) + +```INI +$ ansible-playbook napalm_get_nxos.yml + +PLAY [napalm] *************************************** + +TASK [Retrieve get_facts, get_interfaces] *********** +ok: [nxos1] + +TASK [debug] **************************************** +ok: [nxos1] => { + "napalm_facts": { + "fqdn": "nxos1.domain.com", + "hostname": "nxos1", + "interface_list": [ + "mgmt0", + "Ethernet1/1", + "Ethernet1/2", + "Ethernet1/3", + "Ethernet1/4", + "Vlan1" + ], + "model": "Nexus9000 9000v Chassis", + "os_version": "", + "serial_number": "9B00000000S", + "uptime": 12767664, + "vendor": "Cisco" + } +} + +PLAY RECAP ****************************************** +nxos1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +#### Playbook (NX-OS SSH) + +```YAML +--- +- name: napalm nxos_ssh + hosts: nxos1 + tasks: + - name: Retrieve get_facts, get_interfaces + napalm.napalm.get_facts: + filter: facts,interfaces + # Instruct NAPALM module to use SSH + dev_os: nxos_ssh + + - debug: + var: napalm_facts +``` + +#### Playbook Output (NX-OS SSH) + +```INI +$ ansible-playbook napalm_get_nxos_ssh.yml + +PLAY [napalm nxos_ssh] ******************************** + +TASK [Retrieve get_facts, get_interfaces] ************* +ok: [nxos1] + +TASK [debug] ****************************************** +ok: [nxos1] => { + "napalm_facts": { + "fqdn": "nxos1.domain.com", + "hostname": "nxos1", + "interface_list": [ + "mgmt0", + "Ethernet1/1", + "Ethernet1/2", + "Ethernet1/3", + "Ethernet1/4", + "Vlan1" + ], + "model": "Nexus9000 9000v Chassis", + "os_version": "9.2(3)", + "serial_number": "9000000000S", + "uptime": 12767973, + "vendor": "Cisco" + } +} + +PLAY RECAP ******************************************** +nxos1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Juniper Junos + +#### Inventory (Junos) + +```INI +[juniper] +juniper1 ansible_host=juniper1.domain.com + +[juniper:vars] +# Must match Python that NAPALM is installed into. +ansible_python_interpreter=/path/to/venv/bin/python +ansible_network_os=junos +# Continue using 'network_cli' (NAPALM module itself will use NETCONF) +ansible_connection=network_cli +ansible_user=admin +ansible_ssh_pass=my_password +``` + +#### Playbook (Junos) + +```YAML +--- +- name: napalm + hosts: juniper + gather_facts: False + tasks: + - name: Retrieve get_facts, get_interfaces + napalm.napalm.get_facts: + filter: facts,interfaces + + - debug: + var: napalm_facts +``` + +#### Playbook Output (Junos) + +```INI +$ ansible-playbook napalm_get_junos.yml -i + +PLAY [napalm] ***************************************** + +TASK [Retrieve get_facts, get_interfaces] ************* +ok: [juniper1] + +TASK [debug] ****************************************** +ok: [juniper1] => { + "napalm_facts": { + "fqdn": "juniper1", + "hostname": "juniper1", + "interface_list": [ + "fe-0/0/0", + "gr-0/0/0", + "ip-0/0/0", + "lt-0/0/0", + "mt-0/0/0", + "sp-0/0/0", + "fe-0/0/1", + "fe-0/0/2", + "fe-0/0/3", + "fe-0/0/4", + "fe-0/0/5", + "fe-0/0/6", + "fe-0/0/7", + "gre", + "ipip", + "irb", + "lo0", + "lsi", + "mtun", + "pimd", + "pime", + "pp0", + "ppd0", + "ppe0", + "st0", + "tap", + "vlan" + ], + "model": "SRX100H2", + "os_version": "12.1X44-D35.5", + "serial_number": "BZ0000000008", + "uptime": 119586097, + "vendor": "Juniper" + } +} + +PLAY RECAP ******************************************* +juniper1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Example to install config on a device + +```INI +- assemble: + src=../compiled/{{ inventory_hostname }}/ + dest=../compiled/{{ inventory_hostname }}/running.conf + + - napalm.napalm.install_config: + hostname={{ inventory_hostname }} + username={{ user }} + dev_os={{ os }} + password={{ passwd }} + config_file=../compiled/{{ inventory_hostname }}/running.conf + commit_changes={{ commit_changes }} + replace_config={{ replace_config }} + get_diffs=True + diff_file=../compiled/{{ inventory_hostname }}/diff +``` + +### Example to get compliance report + +```YAML +- name: GET VALIDATION REPORT + napalm.napalm.validate: + username: "{{ user }}" + password: "{{ passwd }}" + hostname: "{{ inventory_hostname }}" + dev_os: "{{ dev_os }}" + validation_file: validate.yml +``` + diff --git a/collection/build.sh b/collection/build.sh new file mode 100644 index 0000000..1d6e6b9 --- /dev/null +++ b/collection/build.sh @@ -0,0 +1,4 @@ +ansible-galaxy collection build +mv napalm-napalm-0.9.12.tar.gz build/ +# rm -r ~/.ansible/collections/ansible_collections/napalm +ansible-galaxy collection install build/napalm-napalm-0.9.12.tar.gz diff --git a/collection/build/napalm-napalm-0.9.12.tar.gz b/collection/build/napalm-napalm-0.9.12.tar.gz new file mode 100644 index 0000000..20ede34 Binary files /dev/null and b/collection/build/napalm-napalm-0.9.12.tar.gz differ diff --git a/collection/build/napalm-napalm-0.9.13.tar.gz b/collection/build/napalm-napalm-0.9.13.tar.gz new file mode 100644 index 0000000..87a82f7 Binary files /dev/null and b/collection/build/napalm-napalm-0.9.13.tar.gz differ diff --git a/collection/galaxy.yml b/collection/galaxy.yml new file mode 100644 index 0000000..1034eb1 --- /dev/null +++ b/collection/galaxy.yml @@ -0,0 +1,21 @@ +--- +namespace: napalm +name: napalm +version: 0.9.13 +readme: README.md +authors: + - Kirk Byers + - Mircea Ulinic + - David Barroso +description: NAPALM + Ansible Modules +license_file: LICENSE +tags: + - networking + - napalm +repository: https://github.com/napalm-automation/napalm-ansible +documentation: https://github.com/napalm-automation/napalm-ansible +homepage: https://github.com/napalm-automation/napalm-ansible +issues: https://github.com/napalm-automation/napalm-ansible/issues +build_ignore: + - "*.tar.gz" + - "build.sh" diff --git a/collection/meta/runtime.yml b/collection/meta/runtime.yml new file mode 100644 index 0000000..349176d --- /dev/null +++ b/collection/meta/runtime.yml @@ -0,0 +1,37 @@ +--- +requires_ansible: '>=2.9.10' +plugin_routing: + action: + get_facts: + redirect: napalm.napalm.napalm_get_facts + install_config: + redirect: napalm.napalm.napalm_install_config + ping: + redirect: napalm.napalm.napalm_ping + validate: + redirect: napalm.napalm.napalm_validate + cli: + redirect: napalm.napalm.napalm_cli + parse_yang: + redirect: napalm.napalm.napalm_parse_yang + translate_yang: + redirect: napalm.napalm.napalm_translate_yang + diff_yang: + redirect: napalm.napalm.napalm_diff_yang + modules: + get_facts: + redirect: napalm.napalm.napalm_get_facts + install_config: + redirect: napalm.napalm.napalm_install_config + ping: + redirect: napalm.napalm.napalm_ping + validate: + redirect: napalm.napalm.napalm_validate + cli: + redirect: napalm.napalm.napalm_cli + parse_yang: + redirect: napalm.napalm.napalm_parse_yang + translate_yang: + redirect: napalm.napalm.napalm_translate_yang + diff_yang: + redirect: napalm.napalm.napalm_diff_yang diff --git a/collection/plugins/action/__init__.py b/collection/plugins/action/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/collection/plugins/action/napalm.py b/collection/plugins/action/napalm.py new file mode 100644 index 0000000..1952b26 --- /dev/null +++ b/collection/plugins/action/napalm.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +__metaclass__ = type + +from ansible.plugins.action.normal import ActionModule as _ActionModule + + +class ActionModule(_ActionModule): + def run(self, tmp=None, task_vars=None): + pc = self._play_context + + if hasattr(pc, "connection_user"): + # populate provider values with context values if not set + provider = self._task.args.get("provider", {}) + + provider["hostname"] = provider.get( + "hostname", provider.get("host", pc.remote_addr) + ) + username = provider.get("username", pc.connection_user) + # Try to make ansible_connection=network_cli also work + if not username: + username = provider.get("username", pc.remote_user) + provider["username"] = username + provider["password"] = provider.get("password", pc.password) + # Timeout can't be passed via command-line as Ansible defaults to a 10 second timeout + provider["timeout"] = provider.get("timeout", 60) + + if hasattr(pc, "network_os"): + provider["dev_os"] = provider.get("dev_os", pc.network_os) + + self._task.args["provider"] = provider + + result = super(ActionModule, self).run(tmp, task_vars) + return result diff --git a/collection/plugins/action/napalm_get_facts.py b/collection/plugins/action/napalm_get_facts.py new file mode 120000 index 0000000..20e7be0 --- /dev/null +++ b/collection/plugins/action/napalm_get_facts.py @@ -0,0 +1 @@ +napalm.py \ No newline at end of file diff --git a/collection/plugins/action/napalm_install_config.py b/collection/plugins/action/napalm_install_config.py new file mode 120000 index 0000000..20e7be0 --- /dev/null +++ b/collection/plugins/action/napalm_install_config.py @@ -0,0 +1 @@ +napalm.py \ No newline at end of file diff --git a/collection/plugins/action/napalm_parse_yang.py b/collection/plugins/action/napalm_parse_yang.py new file mode 120000 index 0000000..20e7be0 --- /dev/null +++ b/collection/plugins/action/napalm_parse_yang.py @@ -0,0 +1 @@ +napalm.py \ No newline at end of file diff --git a/collection/plugins/action/napalm_ping.py b/collection/plugins/action/napalm_ping.py new file mode 120000 index 0000000..20e7be0 --- /dev/null +++ b/collection/plugins/action/napalm_ping.py @@ -0,0 +1 @@ +napalm.py \ No newline at end of file diff --git a/collection/plugins/action/napalm_validate.py b/collection/plugins/action/napalm_validate.py new file mode 120000 index 0000000..20e7be0 --- /dev/null +++ b/collection/plugins/action/napalm_validate.py @@ -0,0 +1 @@ +napalm.py \ No newline at end of file diff --git a/collection/plugins/modules/__init__.py b/collection/plugins/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/collection/plugins/modules/napalm_cli.py b/collection/plugins/modules/napalm_cli.py new file mode 100644 index 0000000..0f25c11 --- /dev/null +++ b/collection/plugins/modules/napalm_cli.py @@ -0,0 +1,191 @@ +from __future__ import unicode_literals, print_function +from ansible.module_utils.basic import AnsibleModule + + +# FIX for Ansible 2.8 moving this function and making it private +# greatly simplified for napalm-ansible's use +def return_values(obj): + """Return native stringified values from datastructures. + + For use with removing sensitive values pre-jsonification.""" + yield str(obj) + + +DOCUMENTATION = """ +--- +module: napalm_cli +author: "Charlie Allom" +version_added: "2.2" +short_description: "Executes network device CLI commands and returns response using NAPALM" +description: + - "Executes network device CLI commands and returns response using NAPALM" +requirements: + - napalm +options: + hostname: + description: + - IP or FQDN of the device you want to connect to + required: False + username: + description: + - Username + required: False + password: + description: + - Password + required: False + args: + description: + - Keyword arguments to pass to the `cli` method + required: True + dev_os: + description: + - OS of the device + required: False + provider: + description: + - Dictionary which acts as a collection of arguments used to define the characteristics + of how to connect to the device. + Note - hostname, username, password and dev_os must be defined in either provider + or local param + Note - local param takes precedence, e.g. hostname is preferred to provider['hostname'] + required: False + +""" + +EXAMPLES = """ +- napalm_cli: + hostname: "{{ inventory_hostname }}" + username: "napalm" + password: "napalm" + dev_os: "eos" + args: + commands: + - show version + - show snmp chassis + +- napalm_cli: + provider: "{{ napalm_provider }}" + args: + commands: + - show version + - show snmp chassis +""" + +RETURN = """ +changed: + description: ALWAYS RETURNS FALSE + returned: always + type: bool + sample: True +results: + description: string of command output + returned: always + type: dict + sample: '{ + "show snmp chassis": "Chassis: 1234\n", + "show version": "Arista vEOS\nHardware version: \nSerial number: \nSystem MAC address: 0800.27c3.5f28\n\nSoftware image version: 4.17.5M\nArchitecture: i386\nInternal build version: 4.17.5M-4414219.4175M\nInternal build ID: d02143c6-e42b-4fc3-99b6-97063bddb6b8\n\nUptime: 1 hour and 21 minutes\nTotal memory: 1893416 kB\nFree memory: 956488 kB\n\n" # noqa + }' +""" + +napalm_found = False +try: + from napalm import get_network_driver + from napalm.base import ModuleImportError + + napalm_found = True +except ImportError: + pass + + +def main(): + module = AnsibleModule( + argument_spec=dict( + hostname=dict(type="str", required=False, aliases=["host"]), + username=dict(type="str", required=False), + password=dict(type="str", required=False, no_log=True), + provider=dict(type="dict", required=False), + timeout=dict(type="int", required=False, default=60), + dev_os=dict(type="str", required=False), + optional_args=dict(required=False, type="dict", default=None), + args=dict(required=True, type="dict", default=None), + ), + supports_check_mode=False, + ) + + if not napalm_found: + module.fail_json(msg="the python module napalm is required") + + provider = module.params["provider"] or {} + + no_log = ["password", "secret"] + for param in no_log: + if provider.get(param): + module.no_log_values.update(return_values(provider[param])) + if provider.get("optional_args") and provider["optional_args"].get(param): + module.no_log_values.update( + return_values(provider["optional_args"].get(param)) + ) + if module.params.get("optional_args") and module.params["optional_args"].get( + param + ): + module.no_log_values.update( + return_values(module.params["optional_args"].get(param)) + ) + + # allow host or hostname + provider["hostname"] = provider.get("hostname", None) or provider.get("host", None) + # allow local params to override provider + for param, pvalue in provider.items(): + if module.params.get(param) is not False: + module.params[param] = module.params.get(param) or pvalue + + hostname = module.params["hostname"] + username = module.params["username"] + dev_os = module.params["dev_os"] + password = module.params["password"] + timeout = module.params["timeout"] + args = module.params["args"] + + argument_check = {"hostname": hostname, "username": username, "dev_os": dev_os} + for key, val in argument_check.items(): + if val is None: + module.fail_json(msg=str(key) + " is required") + + if module.params["optional_args"] is None: + optional_args = {} + else: + optional_args = module.params["optional_args"] + + try: + network_driver = get_network_driver(dev_os) + except ModuleImportError as e: + module.fail_json(msg="Failed to import napalm driver: " + str(e)) + + try: + device = network_driver( + hostname=hostname, + username=username, + password=password, + timeout=timeout, + optional_args=optional_args, + ) + device.open() + except Exception as e: + module.fail_json(msg="cannot connect to device: " + str(e)) + + try: + cli_response = device.cli(**args) + except Exception as e: + module.fail_json(msg="{}".format(e)) + + try: + device.close() + except Exception as e: + module.fail_json(msg="cannot close device connection: " + str(e)) + + module.exit_json(changed=False, cli_results=cli_response) + + +if __name__ == "__main__": + main() diff --git a/collection/plugins/modules/napalm_diff_yang.py b/collection/plugins/modules/napalm_diff_yang.py new file mode 100644 index 0000000..8fdcb6a --- /dev/null +++ b/collection/plugins/modules/napalm_diff_yang.py @@ -0,0 +1,128 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +(c) 2017 David Barroso + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" +from __future__ import unicode_literals, print_function +from ansible.module_utils.basic import AnsibleModule + +try: + import napalm_yang +except ImportError: + napalm_yang = None + + +DOCUMENTATION = """ +--- +module: napalm_diff_yang +author: "David Barroso (@dbarrosop)" +version_added: "0.0" +short_description: "Return diff of two YANG objects" +description: + - "Create two YANG objects from dictionaries and runs mehtod" + - "napalm_yang.utils.diff on them." +requirements: + - napalm-yang +options: + models: + description: + - List of models to parse + required: True + first: + description: + - Dictionary with the data to load into the first YANG object + required: True + second: + description: + - Dictionary with the data to load into the second YANG object + required: True +""" + +EXAMPLES = """ +- napalm_diff_yang: + first: "{{ candidate.yang_model }}" + second: "{{ running_config.yang_model }}" + models: + - models.openconfig_interfaces + register: diff +""" + +RETURN = """ +diff: + description: "Same output as the method napalm_yang.utils.diff" + returned: always + type: dict + sample: '{ + "interfaces": { + "interface": { + "both": { + "Port-Channel1": { + "config": { + "description": { + "first": "blah", + "second": "Asadasd" + } + } + } + } + } + }' +""" + + +def get_root_object(models): + """ + Read list of models and returns a Root object with the proper models added. + """ + root = napalm_yang.base.Root() + + for model in models: + current = napalm_yang + for p in model.split("."): + current = getattr(current, p) + root.add_model(current) + + return root + + +def main(): + module = AnsibleModule( + argument_spec=dict( + models=dict(type="list", required=True), + first=dict(type="dict", required=True), + second=dict(type="dict", required=True), + ), + supports_check_mode=True, + ) + + if not napalm_yang: + module.fail_json(msg="the python module napalm-yang is required") + + first = get_root_object(module.params["models"]) + first.load_dict(module.params["first"]) + + second = get_root_object(module.params["models"]) + second.load_dict(module.params["second"]) + + diff = napalm_yang.utils.diff(first, second) + + module.exit_json(yang_diff=diff) + + +if __name__ == "__main__": + main() diff --git a/collection/plugins/modules/napalm_get_facts.py b/collection/plugins/modules/napalm_get_facts.py new file mode 100644 index 0000000..955c7e9 --- /dev/null +++ b/collection/plugins/modules/napalm_get_facts.py @@ -0,0 +1,300 @@ +""" +(c) 2020 Kirk Byers +(c) 2016 Elisa Jasinska + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" +from __future__ import unicode_literals, print_function +from ansible.module_utils.basic import AnsibleModule + + +# FIX for Ansible 2.8 moving this function and making it private +# greatly simplified for napalm-ansible's use +def return_values(obj): + """Return native stringified values from datastructures. + + For use with removing sensitive values pre-jsonification.""" + yield str(obj) + + +DOCUMENTATION = """ +--- +module: napalm_get_facts +author: "Elisa Jasinska (@fooelisa)" +version_added: "2.1" +short_description: "Gathers facts from a network device via napalm" +description: + - "Gathers facts from a network device via the Python module napalm" +requirements: + - napalm +options: + hostname: + description: + - IP or FQDN of the device you want to connect to + required: False + username: + description: + - Username + required: False + password: + description: + - Password + required: False + dev_os: + description: + - OS of the device + required: False + provider: + description: + - Dictionary which acts as a collection of arguments used to define the characteristics + of how to connect to the device. + Note - hostname, username, password and dev_os must be defined in either provider + or local param + Note - local param takes precedence, e.g. hostname is preferred to provider['hostname'] + required: False + timeout: + description: + - Time in seconds to wait for the device to respond + required: False + default: 60 + optional_args: + description: + - Dictionary of additional arguments passed to underlying driver + required: False + default: None + ignore_notimplemented: + description: + - "Ignores NotImplementedError for filters which aren't supported by the driver. Returns + invalid filters in a list called: not_implemented" + required: False + default: False + choices: [True, False] + filter: + description: + - "A list of facts to retreive from a device and provided though C(ansible_facts) + The list of facts available are maintained at: + http://napalm.readthedocs.io/en/latest/support/ + Note- not all getters are implemented on all supported device types" + required: False + default: ['facts'] + args: + description: + - dictionary of kwargs arguments to pass to the filter. The outer key is the name of + the getter (same as the filter) + required: False + default: None +""" + +EXAMPLES = """ +- name: get facts from device + napalm_get_facts: + hostname: '{{ inventory_hostname }}' + username: '{{ user }}' + dev_os: '{{ os }}' + password: '{{ passwd }}' + filter: ['facts'] + register: result + +- name: print data + debug: + var: result + +- name: Getters + napalm_get_facts: + provider: "{{ ios_provider }}" + filter: + - "lldp_neighbors_detail" + - "interfaces" + +- name: get facts from device + napalm_get_facts: + hostname: "{{ host }}" + username: "{{ user }}" + dev_os: "{{ os }}" + password: "{{ password }}" + optional_args: + port: "{{ port }}" + filter: ['facts', 'route_to', 'interfaces'] + args: + route_to: + protocol: static + destination: 8.8.8.8 + +""" + +RETURN = """ +changed: + description: "whether the command has been executed on the device" + returned: always + type: bool + sample: True +ansible_facts: + description: "Facts gathered on the device provided via C(ansible_facts)" + returned: certain keys are returned depending on filter + type: dict +""" + +napalm_found = False +try: + from napalm import get_network_driver + from napalm.base import ModuleImportError + + napalm_found = True +except ImportError: + pass + + +def main(): + module = AnsibleModule( + argument_spec=dict( + hostname=dict(type="str", required=False, aliases=["host"]), + username=dict(type="str", required=False), + password=dict(type="str", required=False, no_log=True), + provider=dict(type="dict", required=False), + dev_os=dict(type="str", required=False), + timeout=dict(type="int", required=False, default=60), + ignore_notimplemented=dict(type="bool", required=False, default=False), + args=dict(type="dict", required=False, default=None), + optional_args=dict(type="dict", required=False, default=None), + filter=dict(type="list", required=False, default=["facts"]), + ), + supports_check_mode=True, + ) + + if not napalm_found: + module.fail_json(msg="the python module napalm is required") + + provider = module.params["provider"] or {} + + no_log = ["password", "secret"] + for param in no_log: + if provider.get(param): + module.no_log_values.update(return_values(provider[param])) + if provider.get("optional_args") and provider["optional_args"].get(param): + module.no_log_values.update( + return_values(provider["optional_args"].get(param)) + ) + if module.params.get("optional_args") and module.params["optional_args"].get( + param + ): + module.no_log_values.update( + return_values(module.params["optional_args"].get(param)) + ) + + # allow host or hostname + provider["hostname"] = provider.get("hostname", None) or provider.get("host", None) + # allow local params to override provider + for param, pvalue in provider.items(): + if module.params.get(param) is not False: + module.params[param] = module.params.get(param) or pvalue + + hostname = module.params["hostname"] + username = module.params["username"] + dev_os = module.params["dev_os"] + password = module.params["password"] + timeout = module.params["timeout"] + filter_list = module.params["filter"] + args = module.params["args"] or {} + ignore_notimplemented = module.params["ignore_notimplemented"] + implementation_errors = [] + + argument_check = {"hostname": hostname, "username": username, "dev_os": dev_os} + for key, val in argument_check.items(): + if val is None: + module.fail_json(msg=str(key) + " is required") + + if module.params["optional_args"] is None: + optional_args = {} + else: + optional_args = module.params["optional_args"] + + try: + network_driver = get_network_driver(dev_os) + except ModuleImportError as e: + module.fail_json(msg="Failed to import napalm driver: " + str(e)) + + try: + device = network_driver( + hostname=hostname, + username=username, + password=password, + timeout=timeout, + optional_args=optional_args, + ) + device.open() + except Exception as e: + module.fail_json(msg="cannot connect to device: " + str(e)) + + # retreive data from device + facts = {} + + NAPALM_GETTERS = [ + getter for getter in dir(network_driver) if getter.startswith("get_") + ] + # Allow NX-OS checkpoint file to be retrieved via Ansible for use with replace config + NAPALM_GETTERS.append("get_checkpoint_file") + + for getter in filter_list: + getter_function = "get_{}".format(getter) + if getter_function not in NAPALM_GETTERS: + module.fail_json(msg="filter not recognized: " + getter) + + try: + if getter_function == "get_checkpoint_file": + getter_function = "_get_checkpoint_file" + get_func = getattr(device, getter_function) + result = get_func(**args.get(getter, {})) + facts[getter] = result + except NotImplementedError: + if ignore_notimplemented: + implementation_errors.append(getter) + else: + module.fail_json( + msg="The filter {} is not supported in napalm-{} [get_{}()]".format( + getter, dev_os, getter + ) + ) + except Exception as e: + module.fail_json( + msg="[{}] cannot retrieve device data: ".format(getter) + str(e) + ) + + # close device connection + try: + device.close() + except Exception as e: + module.fail_json(msg="cannot close device connection: " + str(e)) + + new_facts = {} + # Prepend all facts with napalm_ for unique namespace + for filter_name, filter_value in facts.items(): + # Make napalm get_facts to be directly accessible as variables + if filter_name == "facts": + for fact_name, fact_value in filter_value.items(): + napalm_fact_name = "napalm_" + fact_name + new_facts[napalm_fact_name] = fact_value + new_filter_name = "napalm_" + filter_name + new_facts[new_filter_name] = filter_value + results = {"ansible_facts": new_facts} + + if ignore_notimplemented: + results["not_implemented"] = sorted(implementation_errors) + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/collection/plugins/modules/napalm_install_config.py b/collection/plugins/modules/napalm_install_config.py new file mode 100644 index 0000000..ff03288 --- /dev/null +++ b/collection/plugins/modules/napalm_install_config.py @@ -0,0 +1,340 @@ +""" +(c) 2020 Kirk Byers +(c) 2016 Elisa Jasinska + Original prototype by David Barroso + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" +from __future__ import unicode_literals, print_function +import os.path +from ansible.module_utils.basic import AnsibleModule + + +# FIX for Ansible 2.8 moving this function and making it private +# greatly simplified for napalm-ansible's use +def return_values(obj): + """Return native stringified values from datastructures. + + For use with removing sensitive values pre-jsonification.""" + yield str(obj) + + +DOCUMENTATION = """ +--- +module: napalm_install_config +author: "Elisa Jasinska (@fooelisa)" +version_added: "2.1" +short_description: "Installs the configuration taken from a file on a device supported by NAPALM" +description: + - "This library will take the configuration from a file and load it into a device running any + OS supported by napalm. The old configuration will be replaced or merged with the new one." +requirements: + - napalm +options: + hostname: + description: + - IP or FQDN of the device you want to connect to + required: False + username: + description: + - Username + required: False + password: + description: + - Password + required: False + provider: + description: + - Dictionary which acts as a collection of arguments used to define the characteristics + of how to connect to the device. Connection arguments can be inferred from inventory + and CLI arguments or specified in a provider or specified individually. + required: False + dev_os: + description: + - OS of the device + required: False + timeout: + description: + - Time in seconds to wait for the device to respond + required: False + default: 60 + optional_args: + description: + - Dictionary of additional arguments passed to underlying driver + required: False + default: None + config_file: + description: + - Path to the file to load the configuration from. Either config or config_file is needed. + required: False + config: + description: + - Configuration to load. Either config or config_file is needed. + required: False + commit_changes: + description: + - If set to True the configuration will be actually merged or replaced. If the set to + False, we will not apply the changes, just check and report the diff + choices: [true,false] + required: True + replace_config: + description: + - If set to True, the entire configuration on the device will be replaced during the + commit. If set to False, we will merge the new config with the existing one. + choices: [true,false] + default: False + required: False + diff_file: + description: + - A path to the file where we store the "diff" between the running configuration and the + new configuration. If not set the diff between configurations will not be saved. + default: None + required: False + get_diffs: + description: + - Set to False to not have any diffs generated. Useful if platform does not support + commands being used to generate diffs. Note- By default diffs are generated even + if the diff_file param is not set. + choices: [true,false] + default: True + required: False + archive_file: + description: + - File to store backup of running-configuration from device. Configuration will not be + retrieved if not set. + default: None + required: False + candidate_file: + description: + - Store a backup of candidate config from device prior to a commit. + default: None + required: False +""" + +EXAMPLES = """ +- assemble: + src: '../compiled/{{ inventory_hostname }}/' + dest: '../compiled/{{ inventory_hostname }}/running.conf' + +- name: Install Config and save diff + napalm_install_config: + hostname: '{{ inventory_hostname }}' + username: '{{ user }}' + dev_os: '{{ os }}' + password: '{{ passwd }}' + config_file: '../compiled/{{ inventory_hostname }}/running.conf' + commit_changes: '{{ commit_changes }}' + replace_config: '{{ replace_config }}' + get_diffs: True + diff_file: '../compiled/{{ inventory_hostname }}/diff' + +- name: Install Config using Provider + napalm_install_config: + provider: "{{ ios_provider }}" + config_file: '../compiled/{{ inventory_hostname }}/running.conf' + commit_changes: '{{ commit_changes }}' + replace_config: '{{ replace_config }}' + get_diffs: True + diff_file: '../compiled/{{ inventory_hostname }}/diff' +""" + +RETURN = """ +changed: + description: whether the config on the device was changed + returned: always + type: bool + sample: True +diff: + description: diff of the change + returned: always + type: dict + sample: { + 'prepared': "[edit system]\n- host-name lab-testing;\n+ host-name lab;", + } +""" + +napalm_found = False +try: + from napalm import get_network_driver + from napalm.base import ModuleImportError + + napalm_found = True +except ImportError: + pass + + +def save_to_file(content, filename): + with open(filename, "w") as f: + f.write(content) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + hostname=dict(type="str", required=False, aliases=["host"]), + username=dict(type="str", required=False), + password=dict(type="str", required=False, no_log=True), + provider=dict(type="dict", required=False), + timeout=dict(type="int", required=False, default=60), + optional_args=dict(required=False, type="dict", default=None), + config_file=dict(type="str", required=False), + config=dict(type="str", required=False), + dev_os=dict(type="str", required=False), + commit_changes=dict(type="bool", required=True), + replace_config=dict(type="bool", required=False, default=False), + diff_file=dict(type="str", required=False, default=None), + get_diffs=dict(type="bool", required=False, default=True), + archive_file=dict(type="str", required=False, default=None), + candidate_file=dict(type="str", required=False, default=None), + ), + supports_check_mode=True, + ) + + if not napalm_found: + module.fail_json(msg="the python module napalm is required") + + provider = module.params["provider"] or {} + + no_log = ["password", "secret"] + for param in no_log: + if provider.get(param): + module.no_log_values.update(return_values(provider[param])) + if provider.get("optional_args") and provider["optional_args"].get(param): + module.no_log_values.update( + return_values(provider["optional_args"].get(param)) + ) + if module.params.get("optional_args") and module.params["optional_args"].get( + param + ): + module.no_log_values.update( + return_values(module.params["optional_args"].get(param)) + ) + + # allow host or hostname + provider["hostname"] = provider.get("hostname", None) or provider.get("host", None) + # allow local params to override provider + for param, pvalue in provider.items(): + if module.params.get(param) is not False: + module.params[param] = module.params.get(param) or pvalue + + hostname = module.params["hostname"] + username = module.params["username"] + dev_os = module.params["dev_os"] + password = module.params["password"] + timeout = module.params["timeout"] + config_file = module.params["config_file"] + config = module.params["config"] + commit_changes = module.params["commit_changes"] + replace_config = module.params["replace_config"] + diff_file = module.params["diff_file"] + get_diffs = module.params["get_diffs"] + archive_file = module.params["archive_file"] + candidate_file = module.params["candidate_file"] + if config_file: + config_file = os.path.expanduser(os.path.expandvars(config_file)) + if diff_file: + diff_file = os.path.expanduser(os.path.expandvars(diff_file)) + if archive_file: + archive_file = os.path.expanduser(os.path.expandvars(archive_file)) + if candidate_file: + candidate_file = os.path.expanduser(os.path.expandvars(candidate_file)) + + argument_check = {"hostname": hostname, "username": username, "dev_os": dev_os} + for key, val in argument_check.items(): + if val is None: + module.fail_json(msg=str(key) + " is required") + + if module.params["optional_args"] is None: + optional_args = {} + else: + optional_args = module.params["optional_args"] + + try: + network_driver = get_network_driver(dev_os) + except ModuleImportError as e: + module.fail_json(msg="Failed to import napalm driver: " + str(e)) + + try: + device = network_driver( + hostname=hostname, + username=username, + password=password, + timeout=timeout, + optional_args=optional_args, + ) + device.open() + except Exception as e: + module.fail_json(msg="cannot connect to device: " + str(e)) + + try: + if archive_file is not None: + running_config = device.get_config(retrieve="running")["running"] + save_to_file(running_config, archive_file) + except Exception as e: + module.fail_json(msg="cannot retrieve running config:" + str(e)) + + try: + if replace_config and config_file: + device.load_replace_candidate(filename=config_file) + elif replace_config and config: + device.load_replace_candidate(config=config) + elif not replace_config and config_file: + device.load_merge_candidate(filename=config_file) + elif not replace_config and config: + device.load_merge_candidate(config=config) + else: + module.fail_json(msg="You have to specify either config or config_file") + except Exception as e: + module.fail_json(msg="cannot load config: " + str(e)) + + try: + if get_diffs: + diff = device.compare_config() + changed = len(diff) > 0 + else: + changed = True + diff = None + if diff_file is not None and get_diffs: + save_to_file(diff, diff_file) + except Exception as e: + module.fail_json(msg="cannot diff config: " + str(e)) + + try: + if candidate_file is not None: + running_config = device.get_config(retrieve="candidate")["candidate"] + save_to_file(running_config, candidate_file) + except Exception as e: + module.fail_json(msg="cannot retrieve running config:" + str(e)) + + try: + if module.check_mode or not commit_changes: + device.discard_config() + else: + if changed: + device.commit_config() + except Exception as e: + module.fail_json(msg="cannot install config: " + str(e)) + + try: + device.close() + except Exception as e: + module.fail_json(msg="cannot close device connection: " + str(e)) + + module.exit_json(changed=changed, diff={"prepared": diff}, msg=diff) + + +if __name__ == "__main__": + main() diff --git a/collection/plugins/modules/napalm_parse_yang.py b/collection/plugins/modules/napalm_parse_yang.py new file mode 100644 index 0000000..65364fa --- /dev/null +++ b/collection/plugins/modules/napalm_parse_yang.py @@ -0,0 +1,305 @@ +""" +(c) 2017 David Barroso + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" +from __future__ import unicode_literals, print_function +from ansible.module_utils.basic import AnsibleModule +import json + +napalm_found = False +try: + from napalm import get_network_driver + from napalm.base import ModuleImportError + + napalm_found = True +except ImportError: + pass + +try: + import napalm_yang +except ImportError: + napalm_yang = None + + +# FIX for Ansible 2.8 moving this function and making it private +# greatly simplified for napalm-ansible's use +def return_values(obj): + """Return native stringified values from datastructures. + + For use with removing sensitive values pre-jsonification.""" + yield str(obj) + + +DOCUMENTATION = """ +--- +module: napalm_parse_yang +author: "David Barroso (@dbarrosop)" +version_added: "0.0" +short_description: "Parse native config/state from a file or device." +description: + - "Parse configuration/state from a file or device and returns a dict that" + - "represents a valid YANG object." +requirements: + - napalm +options: + hostname: + description: + - IP or FQDN of the device you want to connect to + required: False + username: + description: + - Username + required: False + password: + description: + - Password + required: False + dev_os: + description: + - OS of the device + required: False + provider: + description: + - Dictionary which acts as a collection of arguments used to define + the characteristics of how to connect to the device. + Note - hostname, username, password and dev_os must be defined in + either provider or local param if we want to parse from a device + Note - local param takes precedence, e.g. hostname is preferred to + provider['hostname'] + required: False + timeout: + description: + - Time in seconds to wait for the device to respond + required: False + default: 60 + optional_args: + description: + - Dictionary of additional arguments passed to underlying driver + required: False + default: None + file_path: + description: + - "Path to a file to load native config/state from. + Note: Either file_path or data to connect to a device must be + provided. + Note: file_path takes precedence over a live device" + required: False + defaut: None + mode: + description: + - "Whether to parse config/state or both. + Note: `both` is not supported in combination with `file_path`." + required: True + choices: ['config', 'state', 'both'] + models: + description: + - A list that should match the SUPPORTED_MODELS in napalm-yang + required: True + choices: "" + profiles: + description: + - A list profiles + required: False + choices: "" +""" + +EXAMPLES = """ +- name: Parse from device + napalm_parse_yang: + hostname: '{{ inventory_hostname }}' + username: '{{ user }}' + dev_os: '{{ os }}' + password: '{{ passwd }}' + mode: "config" + profiles: ["eos"] + models: + - models.openconfig_interfaces + register: running + +- name: Parse from file + napalm_parse_yang: + file_path: "eos.config" + mode: "config" + profiles: ["eos"] + models: + - models.openconfig_interfaces + register: config +""" + +RETURN = """ +changed: + description: "Dict the representes a valid YANG object" + returned: always + type: dict + sample: "{'interfaces': {'interface':'Et1': {...}, ... }}" +""" + + +def update_module_provider_data(module): + provider = module.params["provider"] or {} + + no_log = ["password", "secret"] + for param in no_log: + if provider.get(param): + module.no_log_values.update(return_values(provider[param])) + if provider.get("optional_args") and provider["optional_args"].get(param): + module.no_log_values.update( + return_values(provider["optional_args"].get(param)) + ) + if module.params.get("optional_args") and module.params["optional_args"].get( + param + ): + module.no_log_values.update( + return_values(module.params["optional_args"].get(param)) + ) + + # allow host or hostname + provider["hostname"] = provider.get("hostname", None) or provider.get("host", None) + # allow local params to override provider + for param, pvalue in provider.items(): + if module.params.get(param) is not False: + module.params[param] = module.params.get(param) or pvalue + + +def get_root_object(models): + """ + Read list of models and returns a Root object with the proper models added. + """ + root = napalm_yang.base.Root() + + for model in models: + current = napalm_yang + for p in model.split("."): + current = getattr(current, p) + root.add_model(current) + + return root + + +def parse_from_file(module): + file_path = module.params["file_path"] + models = module.params["models"] + mode = module.params["mode"] + profiles = module.params["profiles"] + + root = get_root_object(models) + + with open(file_path, "r") as f: + native = f.read() + try: + native = json.loads(native) + except ValueError: + native = [native] + + if mode == "config": + root.parse_config(native=native, profile=profiles) + elif mode == "state": + root.parse_state(native=native, profile=profiles) + else: + module.fail_json(msg="You can't parse both at the same time from a file") + return root + + +def parse_from_device(module): + update_module_provider_data(module) + + hostname = module.params["hostname"] + username = module.params["username"] + password = module.params["password"] + timeout = module.params["timeout"] + models = module.params["models"] + mode = module.params["mode"] + profiles = module.params["profiles"] + + dev_os = module.params["dev_os"] + argument_check = {"hostname": hostname, "username": username, "dev_os": dev_os} + for key, val in argument_check.items(): + if val is None: + module.fail_json(msg=str(key) + " is required") + + if module.params["optional_args"] is None: + optional_args = {} + else: + optional_args = module.params["optional_args"] + + try: + network_driver = get_network_driver(dev_os) + except ModuleImportError as e: + module.fail_json(msg="Failed to import napalm driver: " + str(e)) + + try: + device = network_driver( + hostname=hostname, + username=username, + password=password, + timeout=timeout, + optional_args=optional_args, + ) + device.open() + except Exception as e: + module.fail_json(msg="cannot connect to device: {}".format(e)) + + root = get_root_object(models) + + if mode in ["config", "both"]: + root.parse_config(device=device, profile=profiles or device.profile) + + if mode in ["state", "both"]: + root.parse_state(device=device, profile=profiles or device.profile) + + # close device connection + try: + device.close() + except Exception as e: + module.fail_json(msg="cannot close device connection: {}".format(e)) + + return root + + +def main(): + module = AnsibleModule( + argument_spec=dict( + hostname=dict(type="str", required=False, aliases=["host"]), + username=dict(type="str", required=False), + password=dict(type="str", required=False, no_log=True), + provider=dict(type="dict", required=False), + file_path=dict(type="str", required=False), + mode=dict(type="str", required=True, choices=["config", "state", "both"]), + models=dict(type="list", required=True), + profiles=dict(type="list", required=False), + dev_os=dict(type="str", required=False), + timeout=dict(type="int", required=False, default=60), + optional_args=dict(type="dict", required=False, default=None), + ), + supports_check_mode=True, + ) + + if not napalm_found: + module.fail_json(msg="the python module napalm is required") + if not napalm_yang: + module.fail_json(msg="the python module napalm-yang is required") + + if module.params["file_path"]: + yang_model = parse_from_file(module) + else: + yang_model = parse_from_device(module) + + module.exit_json(yang_model=yang_model.to_dict(filter=True)) + + +if __name__ == "__main__": + main() diff --git a/collection/plugins/modules/napalm_ping.py b/collection/plugins/modules/napalm_ping.py new file mode 100644 index 0000000..f9382a2 --- /dev/null +++ b/collection/plugins/modules/napalm_ping.py @@ -0,0 +1,252 @@ +""" +(c) 2017 Jason Edelman + +This file is part of Ansible +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" +from __future__ import unicode_literals, print_function +from ansible.module_utils.basic import AnsibleModule + + +# FIX for Ansible 2.8 moving this function and making it private +# greatly simplified for napalm-ansible's use +def return_values(obj): + """Return native stringified values from datastructures. + + For use with removing sensitive values pre-jsonification.""" + yield str(obj) + + +DOCUMENTATION = """ +--- +module: napalm_ping +author: "Jason Edelman (@jedelman8)" +version_added: "2.2" +short_description: "Executes ping on the device and returns response using NAPALM" +description: + - "This module logs into the device, issues a ping request, and returns the response" +requirements: + - napalm +options: + hostname: + description: + - IP or FQDN of the device you want to connect to + required: False + username: + description: + - Username + required: False + password: + description: + - Password + required: False + provider: + description: + - Dictionary which acts as a collection of arguments used to define the characteristics + of how to connect to the device. + Note - hostname, username, password and dev_os must be defined in either provider + or local param + Note - local param takes precedence, e.g. hostname is preferred to provider['hostname'] + required: False + dev_os: + description: + - OS of the device + required: False + timeout: + description: + - Time in seconds to wait for the device to respond + required: False + default: 60 + optional_args: + description: + - Dictionary of additional arguments passed to underlying driver + required: False + default: None + destination: + description: Host or IP Address of the destination + required: True + source: + description: Source address of echo request + required: False + ttl: + description: Maximum number of hops + required: False + ping_timeout: + description: Maximum seconds to wait after sending final packet + required: False + size: + description: Size of request (bytes) + required: False + count: + description: Number of ping request to send + required: False + vrf: + description: vrf to source the echo request + required: False + +""" + +EXAMPLES = """ +- napalm_ping: + hostname: "{{ inventory_hostname }}" + username: "napalm" + password: "napalm" + dev_os: "eos" + destination: 10.0.0.5 + vrf: MANAGEMENT + count: 2 + +- napalm_ping: + provider: "{{ napalm_provider }}" + destination: 8.8.8.8 + count: 2 +""" + +RETURN = """ +changed: + description: ALWAYS RETURNS FALSE + returned: always + type: bool + sample: True + +results: + description: structure response data of ping + returned: always + type: dict + # when echo request succeeds + sample: '{"success": {"packet_loss": 0, "probes_sent": 2, + "results": [{"ip_address": "10.0.0.5:", "rtt": 1.71}, + {"ip_address": "10.0.0.5:", "rtt": 0.733}], + "rtt_avg": 1.225, "rtt_max": 1.718, "rtt_min": 0.733, + "rtt_stddev": 0.493}}' + +alt_results: + description: Example results key on failure + returned: always + type: dict + # when echo request succeeds + sample: '{"error": "connect: Network is unreachable\n"}}' +""" + +napalm_found = False +try: + from napalm import get_network_driver + from napalm.base import ModuleImportError + + napalm_found = True +except ImportError: + pass + + +def main(): + module = AnsibleModule( + argument_spec=dict( + hostname=dict(type="str", required=False, aliases=["host"]), + username=dict(type="str", required=False), + password=dict(type="str", required=False, no_log=True), + provider=dict(type="dict", required=False), + timeout=dict(type="int", required=False, default=60), + optional_args=dict(required=False, type="dict", default=None), + dev_os=dict(type="str", required=False), + destination=dict(type="str", required=True), + source=dict(type="str", required=False), + ttl=dict(type="str", required=False), + ping_timeout=dict(type="str", required=False), + size=dict(type="str", required=False), + count=dict(type="str", required=False), + vrf=dict(type="str", required=False), + ), + supports_check_mode=True, + ) + + if not napalm_found: + module.fail_json(msg="the python module napalm is required") + + provider = module.params["provider"] or {} + + no_log = ["password", "secret"] + for param in no_log: + if provider.get(param): + module.no_log_values.update(return_values(provider[param])) + if provider.get("optional_args") and provider["optional_args"].get(param): + module.no_log_values.update( + return_values(provider["optional_args"].get(param)) + ) + if module.params.get("optional_args") and module.params["optional_args"].get( + param + ): + module.no_log_values.update( + return_values(module.params["optional_args"].get(param)) + ) + + # allow host or hostname + provider["hostname"] = provider.get("hostname", None) or provider.get("host", None) + # allow local params to override provider + for param, pvalue in provider.items(): + if module.params.get(param) is not False: + module.params[param] = module.params.get(param) or pvalue + + hostname = module.params["hostname"] + username = module.params["username"] + dev_os = module.params["dev_os"] + password = module.params["password"] + timeout = module.params["timeout"] + destination = module.params["destination"] + + ping_optional_args = {} + ping_args = ["source", "ttl", "ping_timeout", "size", "count", "vrf"] + for param, pvalue in module.params.items(): + if param in ping_args and pvalue is not None: + ping_optional_args[param] = pvalue + if "ping_timeout" in ping_optional_args: + ping_optional_args["timeout"] = ping_optional_args["ping_timeout"] + ping_optional_args.pop("ping_timeout") + + argument_check = {"hostname": hostname, "username": username, "dev_os": dev_os} + for key, val in argument_check.items(): + if val is None: + module.fail_json(msg=str(key) + " is required") + + if module.params["optional_args"] is None: + optional_args = {} + else: + optional_args = module.params["optional_args"] + + try: + network_driver = get_network_driver(dev_os) + except ModuleImportError as e: + module.fail_json(msg="Failed to import napalm driver: " + str(e)) + + try: + device = network_driver( + hostname=hostname, + username=username, + password=password, + timeout=timeout, + optional_args=optional_args, + ) + device.open() + except Exception as e: + module.fail_json(msg="cannot connect to device: " + str(e)) + + ping_response = device.ping(destination, **ping_optional_args) + + try: + device.close() + except Exception as e: + module.fail_json(msg="cannot close device connection: " + str(e)) + + module.exit_json(changed=False, ping_results=ping_response) + + +if __name__ == "__main__": + main() diff --git a/collection/plugins/modules/napalm_translate_yang.py b/collection/plugins/modules/napalm_translate_yang.py new file mode 100644 index 0000000..c39f840 --- /dev/null +++ b/collection/plugins/modules/napalm_translate_yang.py @@ -0,0 +1,132 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +(c) 2017 David Barroso + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" +from __future__ import unicode_literals, print_function +from ansible.module_utils.basic import AnsibleModule + +try: + import napalm_yang +except ImportError: + napalm_yang = None + + +DOCUMENTATION = """ +--- +module: napalm_translate_yang +author: "David Barroso (@dbarrosop)" +version_added: "0.0" +short_description: "Translate a YANG object to native configuration" +description: + - "Load a YANG object from a dict and translates the object to native" + - "configuration" +requirements: + - napalm-yang +options: + models: + description: + - List of models to parse + required: True + profiles: + description: + - List of profiles to use to translate the object + required: True + data: + description: + - dict to load into the YANG object + required: True + merge: + description: + - When translating config, merge resulting config here + required: False + replace: + description: + - When translating config, replace resulting config here + required: False +""" + +EXAMPLES = """ +- name: "Translate config" + napalm_translate_yang: + data: "{{ interfaces.yang_model }}" + profiles: ["eos"] + models: + - models.openconfig_interfaces + register: config +""" + +RETURN = """ +config: + description: "Native configuration" + returned: always + type: string + sample: "interface Ethernet2\n no switchport\n ip address 192.168.0.1/24 \n" +""" + + +def get_root_object(models): + """ + Read list of models and returns a Root object with the proper models added. + """ + root = napalm_yang.base.Root() + + for model in models: + current = napalm_yang + for p in model.split("."): + current = getattr(current, p) + root.add_model(current) + + return root + + +def main(): + module = AnsibleModule( + argument_spec=dict( + models=dict(type="list", required=True), + profiles=dict(type="list", required=False), + data=dict(type="dict", required=True), + merge=dict(type="dict", required=False), + replace=dict(type="dict", required=False), + ), + supports_check_mode=True, + ) + + if not napalm_yang: + module.fail_json(msg="the python module napalm-yang is required") + + root = get_root_object(module.params["models"]) + root.load_dict(module.params["data"]) + + running = get_root_object(module.params["models"]) + args = {} + + if module.params["merge"]: + args["merge"] = running + running.load_dict(module.params["merge"]) + elif module.params["replace"]: + args["replace"] = running + running.load_dict(module.params["replace"]) + + config = root.translate_config(profile=module.params["profiles"], **args) + + module.exit_json(config=config) + + +if __name__ == "__main__": + main() diff --git a/collection/plugins/modules/napalm_validate.py b/collection/plugins/modules/napalm_validate.py new file mode 100644 index 0000000..d6d078b --- /dev/null +++ b/collection/plugins/modules/napalm_validate.py @@ -0,0 +1,267 @@ +from __future__ import unicode_literals, print_function +from ansible.module_utils.basic import AnsibleModule + +napalm_found = False +try: + from napalm import get_network_driver + from napalm.base import ModuleImportError + + napalm_found = True +except ImportError: + pass + +try: + import napalm_yang +except ImportError: + napalm_yang = None + + +# FIX for Ansible 2.8 moving this function and making it private +# greatly simplified for napalm-ansible's use +def return_values(obj): + """Return native stringified values from datastructures. + + For use with removing sensitive values pre-jsonification.""" + yield str(obj) + + +DOCUMENTATION = """ +--- +module: napalm_validate +author: Gabriele Gerbino (@GGabriele) +short_description: Performs deployment validation via napalm. +description: + - Performs deployment validation via napalm. +requirements: + - napalm +options: + hostname: + description: + - IP or FQDN of the device you want to connect to. + required: False + username: + description: + - Username. + required: False + password: + description: + - Password. + required: False + dev_os: + description: + - OS of the device. + required: False + provider: + description: + - Dictionary which acts as a collection of arguments used to define + the characteristics of how to connect to the device. + Note - hostname, username, password and dev_os must be defined in + either provider or local param + Note - local param takes precedence, e.g. hostname is preferred + to provider['hostname'] + required: False + timeout: + description: + - Time in seconds to wait for the device to respond. + required: False + default: 60 + optional_args: + description: + - Dictionary of additional arguments passed to underlying driver. + required: False + default: None + validation_file: + description: + - YAML Validation file containing resources desired states. + required: True + models: + description: + - List of models to parse + Note - data to connect to the device is not necessary when using + YANG models + required: True + data: + description: + - dict to load into the YANG object + required: False +""" + +EXAMPLES = """ +- name: GET VALIDATION REPORT + napalm_validate: + username: "{{ un }}" + password: "{{ pwd }}" + hostname: "{{ inventory_hostname }}" + dev_os: "{{ dev_os }}" + validation_file: validate.yml + +- name: GET VALIDATION REPORT USING PROVIDER + napalm_validate: + provider: "{{ ios_provider }}" + validation_file: validate.yml + +# USING YANG +- name: Let's gather state of interfaces + napalm_parse_yang: + dev_os: "{{ dev_os }}" + hostname: "{{ hostname }}" + username: "{{ username }}" + password: "{{ password }}" + mode: "state" + optional_args: + port: "{{ port }}" + models: + - models.openconfig_interfaces + register: interfaces + +- name: Check all interfaces are up + napalm_validate: + data: "{{ interfaces.yang_model }}" + models: + - models.openconfig_interfaces + validation_file: "validate.yaml" + register: report +""" + +RETURN = """ +changed: + description: check to see if a change was made on the device. + returned: always + type: bool + sample: false +compliance_report: + description: validation report obtained via napalm. + returned: always + type: dict +""" + + +def get_compliance_report(module, device): + return device.compliance_report(module.params["validation_file"]) + + +def get_device_instance(module): + + provider = module.params["provider"] or {} + + no_log = ["password", "secret"] + for param in no_log: + if provider.get(param): + module.no_log_values.update(return_values(provider[param])) + if provider.get("optional_args") and provider["optional_args"].get(param): + module.no_log_values.update( + return_values(provider["optional_args"].get(param)) + ) + if module.params.get("optional_args") and module.params["optional_args"].get( + param + ): + module.no_log_values.update( + return_values(module.params["optional_args"].get(param)) + ) + + # allow host or hostname + provider["hostname"] = provider.get("hostname", None) or provider.get("host", None) + # allow local params to override provider + for param, pvalue in provider.items(): + if module.params.get(param) is not False: + module.params[param] = module.params.get(param) or pvalue + + hostname = module.params["hostname"] + username = module.params["username"] + dev_os = module.params["dev_os"] + password = module.params["password"] + timeout = module.params["timeout"] + + argument_check = {"hostname": hostname, "username": username, "dev_os": dev_os} + for key, val in argument_check.items(): + if val is None: + module.fail_json(msg=str(key) + " is required") + + optional_args = module.params["optional_args"] or {} + + try: + network_driver = get_network_driver(dev_os) + except ModuleImportError as e: + module.fail_json(msg="Failed to import napalm driver: " + str(e)) + + try: + device = network_driver( + hostname=hostname, + username=username, + password=password, + timeout=timeout, + optional_args=optional_args, + ) + device.open() + except Exception as err: + module.fail_json(msg="cannot connect to device: {0}".format(str(err))) + return device + + +def get_root_object(models): + """ + Read list of models and returns a Root object with the proper models added. + """ + root = napalm_yang.base.Root() + + for model in models: + current = napalm_yang + for p in model.split("."): + current = getattr(current, p) + root.add_model(current) + + return root + + +def main(): + module = AnsibleModule( + argument_spec=dict( + models=dict(type="list", required=False), + data=dict(type="dict", required=False), + hostname=dict(type="str", required=False, aliases=["host"]), + username=dict(type="str", required=False), + password=dict(type="str", required=False, no_log=True), + provider=dict(type="dict", required=False), + dev_os=dict(type="str", required=False), + timeout=dict(type="int", required=False, default=60), + optional_args=dict(type="dict", required=False, default=None), + validation_file=dict(type="str", required=True), + ), + supports_check_mode=False, + ) + if not napalm_found: + module.fail_json(msg="the python module napalm is required") + + if module.params["models"]: + if not napalm_yang: + module.fail_json(msg="the python module napalm-yang is required") + + device = get_root_object(module.params["models"]) + + if not module.params["data"]: + module.fail_json(msg="You need to pass the data for the YANG obj") + + device.load_dict(module.params["data"]) + else: + device = get_device_instance(module) + compliance_report = get_compliance_report(module, device) + + if not module.params["models"]: + # close device connection + try: + device.close() + except Exception as err: + module.fail_json(msg="cannot close device connection: {0}".format(str(err))) + + results = {} + results["compliance_report"] = compliance_report + if not compliance_report["complies"]: + msg = "Device does not comply with policy" + results["msg"] = msg + module.fail_json(**results) + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/napalm_ansible/modules/napalm_cli.py b/napalm_ansible/modules/napalm_cli.py index d33cba1..0f25c11 100644 --- a/napalm_ansible/modules/napalm_cli.py +++ b/napalm_ansible/modules/napalm_cli.py @@ -5,7 +5,7 @@ # FIX for Ansible 2.8 moving this function and making it private # greatly simplified for napalm-ansible's use def return_values(obj): - """ Return native stringified values from datastructures. + """Return native stringified values from datastructures. For use with removing sensitive values pre-jsonification.""" yield str(obj) diff --git a/napalm_ansible/modules/napalm_get_facts.py b/napalm_ansible/modules/napalm_get_facts.py index 176a202..955c7e9 100644 --- a/napalm_ansible/modules/napalm_get_facts.py +++ b/napalm_ansible/modules/napalm_get_facts.py @@ -24,7 +24,7 @@ # FIX for Ansible 2.8 moving this function and making it private # greatly simplified for napalm-ansible's use def return_values(obj): - """ Return native stringified values from datastructures. + """Return native stringified values from datastructures. For use with removing sensitive values pre-jsonification.""" yield str(obj) diff --git a/napalm_ansible/modules/napalm_install_config.py b/napalm_ansible/modules/napalm_install_config.py index 5f10f55..ff03288 100644 --- a/napalm_ansible/modules/napalm_install_config.py +++ b/napalm_ansible/modules/napalm_install_config.py @@ -26,7 +26,7 @@ # FIX for Ansible 2.8 moving this function and making it private # greatly simplified for napalm-ansible's use def return_values(obj): - """ Return native stringified values from datastructures. + """Return native stringified values from datastructures. For use with removing sensitive values pre-jsonification.""" yield str(obj) diff --git a/napalm_ansible/modules/napalm_parse_yang.py b/napalm_ansible/modules/napalm_parse_yang.py index 4bc939b..65364fa 100644 --- a/napalm_ansible/modules/napalm_parse_yang.py +++ b/napalm_ansible/modules/napalm_parse_yang.py @@ -38,7 +38,7 @@ # FIX for Ansible 2.8 moving this function and making it private # greatly simplified for napalm-ansible's use def return_values(obj): - """ Return native stringified values from datastructures. + """Return native stringified values from datastructures. For use with removing sensitive values pre-jsonification.""" yield str(obj) diff --git a/napalm_ansible/modules/napalm_ping.py b/napalm_ansible/modules/napalm_ping.py index de701c0..f9382a2 100644 --- a/napalm_ansible/modules/napalm_ping.py +++ b/napalm_ansible/modules/napalm_ping.py @@ -20,7 +20,7 @@ # FIX for Ansible 2.8 moving this function and making it private # greatly simplified for napalm-ansible's use def return_values(obj): - """ Return native stringified values from datastructures. + """Return native stringified values from datastructures. For use with removing sensitive values pre-jsonification.""" yield str(obj) diff --git a/napalm_ansible/modules/napalm_validate.py b/napalm_ansible/modules/napalm_validate.py index f4a9f9d..d6d078b 100644 --- a/napalm_ansible/modules/napalm_validate.py +++ b/napalm_ansible/modules/napalm_validate.py @@ -19,7 +19,7 @@ # FIX for Ansible 2.8 moving this function and making it private # greatly simplified for napalm-ansible's use def return_values(obj): - """ Return native stringified values from datastructures. + """Return native stringified values from datastructures. For use with removing sensitive values pre-jsonification.""" yield str(obj) diff --git a/requirements-dev.txt b/requirements-dev.txt index 501e828..04bc21c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ pylama==7.7.1 -pytest -black==18.9b0; python_version >= '3.6' +pytest==5.4.3 +black==20.8b1; python_version >= '3.6' ansible -r requirements.txt diff --git a/requirements.txt b/requirements.txt index 6460676..6785dce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -netmiko==2.4.2; python_version == '2.7' -netmiko>=3.1.0; python_version >= '3.6' -napalm==2.5.0; python_version == '2.7' -napalm>=3.0.1; python_version >= '3.6' +netmiko>=3.3.0; python_version >= '3.6' +napalm>=3.2.0; python_version >= '3.6' six