diff --git a/.travis.yml b/.travis.yml index a3bc78a..a938b16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,20 +2,17 @@ language: python python: - 2.7 - - 3.5 - 3.6 + - 3.7 + - 3.8 env: - - ANSIBLE_VERSION=2.5 - - ANSIBLE_VERSION=2.6 - - ANSIBLE_VERSION=2.7 - ANSIBLE_VERSION=2.8 + - ANSIBLE_VERSION=2.9 install: - - pip install pylama - - pip install -r requirements.txt - - pip install "ansible>=$ANSIBLE_VERSION.0,<$ANSIBLE_VERSION.99" - pip install -r requirements-dev.txt + - pip install "ansible>=$ANSIBLE_VERSION.0,<$ANSIBLE_VERSION.99" - pip install -e . script: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 49103a9..66d4efa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,13 @@ +develop +===== + - Fix issue with -u not working. + - Update tests to use newer Python and to use Ansible 2.8.x or 2.9.x. + - Improving documentation. + - Update module docstring. + - Update napalm_get_facts to allow _get_checkpoint_file for NX-OS + - Update dev tooling to pin dependencies and to install black + - Blacken the code base; exclude from PY27 install + 1.0.0 ===== diff --git a/README.md b/README.md index ab5a211..f1a6512 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Modules The following modules are currently available: +- ``napalm_cli`` - ``napalm_diff_yang`` - ``napalm_get_facts`` - ``napalm_install_config`` @@ -15,11 +16,12 @@ The following modules are currently available: - ``napalm_translate_yang`` - ``napalm_validate`` -Actions +Action-Plugins ======= -Actions will only work with Ansible version 2.3 or greater. -They provides default parameters for the hostname, username, password and timeout paramters. +Action-Plugins should be used to make napalm-ansible more consistent with the behavior of other Ansible modules (eliminate the need of a provider and of individual task arguments for hostname, username, password, and timeout). + +They provide default parameters for the hostname, username, password and timeout paramters. * hostname is set to the first of provider {{ hostname }}, provider {{ host }}, play-context remote_addr. * username is set to the first of provider {{ username }}, play-context connection_user. * password is set to the first of provider {{ password }}, play-context password (-k argument). @@ -28,26 +30,32 @@ They provides default parameters for the hostname, username, password and timeou Install ======= -To install just run the command: +To install run either: ``` pip install napalm-ansible +pip install napalm +``` + +Or: + +``` +git clone https://github.com/napalm-automation/napalm-ansible +pip install napalm ``` -Configuring ansible +Configuring Ansible =================== -Once you have installed ``napalm-ansible`` run the command ``napalm-ansible`` and follow the instructions. For example:: +Once you have installed ``napalm-ansible`` then you need to add napalm-ansible to your ``library`` and ``action_plugins`` paths in ``ansible.cfg``. If you used pip to install napalm-ansible, then you can just run the ``napalm-ansible`` command and follow the instructions specified there. ``` -$ napalm-ansible -To make sure ansible can make use of the napalm modules you will have -to add the following configuration to your ansible configuration -file, i.e. `./ansible.cfg`: +$ cat .ansible.cfg - [defaults] - library = /Users/dbarroso/workspace/napalm/napalm-ansible/napalm_ansible - action_plugins = /Users/dbarroso/workspace/napalm/napalm-ansible/action_plugins +[defaults] +library = ~/napalm-ansible/napalm_ansible/modules +action_plugins = ~/napalm-ansible/napalm_ansible/plugins/action +... For more details on ansible's configuration file visit: https://docs.ansible.com/ansible/latest/intro_configuration.html @@ -55,29 +63,372 @@ https://docs.ansible.com/ansible/latest/intro_configuration.html Dependencies ======= -* [napalm](https://github.com/napalm-automation/napalm) 2.3.0 or later -* [ansible](https://github.com/ansible/ansible) 2.2.0.0 or later +* [napalm](https://github.com/napalm-automation/napalm) 2.5.0 or later +* [ansible](https://github.com/ansible/ansible) 2.8.11 or later Examples ======= -Example to retrieve facts from a device +### 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_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_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 ``` - - name: get facts from device - napalm_get_facts: - hostname={{ inventory_hostname }} - username={{ user }} - dev_os={{ os }} - password={{ passwd }} - filter='facts,interfaces,bgp_neighbors' - register: result - - name: print data - debug: var=result +#### Playbook (NX-OS NX-API) + +```YAML +--- +- name: napalm + hosts: nxos1 + gather_facts: False + tasks: + - name: Retrieve get_facts, get_interfaces + napalm_get_facts: + filter: facts,interfaces + # Specify NX-API Port + optional_args: + port: 8443 + + - debug: + var: napalm_facts ``` -Example to install config on a device + +#### 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_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_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 @@ -94,21 +445,15 @@ Example to install config on a device diff_file=../compiled/{{ inventory_hostname }}/diff ``` -Example to get compliance report -``` +### Example to get compliance report + +```YAML - name: GET VALIDATION REPORT napalm_validate: - username: "{{ un }}" - password: "{{ pwd }}" + username: "{{ user }}" + password: "{{ passwd }}" hostname: "{{ inventory_hostname }}" dev_os: "{{ dev_os }}" validation_file: validate.yml ``` -Example to use default connection parameters: -``` - - name: get facts from device - napalm_get_facts: - dev_os: "{{ os }}" - filter: facts,interfaces,bgp_neighbors -``` diff --git a/napalm_ansible/__init__.py b/napalm_ansible/__init__.py index d175622..b6378c3 100644 --- a/napalm_ansible/__init__.py +++ b/napalm_ansible/__init__.py @@ -19,7 +19,7 @@ def main(): path = os.path.dirname(__file__) - if LooseVersion(ansible.__version__) < LooseVersion('2.3.0.0'): + if LooseVersion(ansible.__version__) < LooseVersion("2.3.0.0"): action_plugins = "" else: action_plugins = "action_plugins = {path}/plugins/action".format(path=path) diff --git a/napalm_ansible/modules/napalm_cli.py b/napalm_ansible/modules/napalm_cli.py index 6097bd7..d33cba1 100644 --- a/napalm_ansible/modules/napalm_cli.py +++ b/napalm_ansible/modules/napalm_cli.py @@ -11,7 +11,7 @@ def return_values(obj): yield str(obj) -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: napalm_cli author: "Charlie Allom" @@ -51,9 +51,9 @@ def return_values(obj): Note - local param takes precedence, e.g. hostname is preferred to provider['hostname'] required: False -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - napalm_cli: hostname: "{{ inventory_hostname }}" username: "napalm" @@ -70,9 +70,9 @@ def return_values(obj): commands: - show version - show snmp chassis -''' +""" -RETURN = ''' +RETURN = """ changed: description: ALWAYS RETURNS FALSE returned: always @@ -86,12 +86,13 @@ def return_values(obj): "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 @@ -100,55 +101,61 @@ def return_values(obj): 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), + 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 + supports_check_mode=False, ) if not napalm_found: module.fail_json(msg="the python module napalm is required") - provider = module.params['provider'] or {} + provider = module.params["provider"] or {} - no_log = ['password', 'secret'] + 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))) + 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) + 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'] + 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} + 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: + if module.params["optional_args"] is None: optional_args = {} else: - optional_args = module.params['optional_args'] + optional_args = module.params["optional_args"] try: network_driver = get_network_driver(dev_os) @@ -156,11 +163,13 @@ def main(): 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 = 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)) @@ -178,5 +187,5 @@ def main(): module.exit_json(changed=False, cli_results=cli_response) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/napalm_ansible/modules/napalm_diff_yang.py b/napalm_ansible/modules/napalm_diff_yang.py index 5e54d15..8fdcb6a 100644 --- a/napalm_ansible/modules/napalm_diff_yang.py +++ b/napalm_ansible/modules/napalm_diff_yang.py @@ -27,7 +27,7 @@ napalm_yang = None -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: napalm_diff_yang author: "David Barroso (@dbarrosop)" @@ -51,18 +51,18 @@ description: - Dictionary with the data to load into the second YANG object required: True -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - napalm_diff_yang: first: "{{ candidate.yang_model }}" second: "{{ running_config.yang_model }}" models: - models.openconfig_interfaces register: diff -''' +""" -RETURN = ''' +RETURN = """ diff: description: "Same output as the method napalm_yang.utils.diff" returned: always @@ -82,7 +82,7 @@ } } }' -''' +""" def get_root_object(models): @@ -104,10 +104,10 @@ 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), + first=dict(type="dict", required=True), + second=dict(type="dict", required=True), ), - supports_check_mode=True + supports_check_mode=True, ) if not napalm_yang: @@ -124,5 +124,5 @@ def main(): module.exit_json(yang_diff=diff) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/napalm_ansible/modules/napalm_get_facts.py b/napalm_ansible/modules/napalm_get_facts.py index d58ddc2..176a202 100644 --- a/napalm_ansible/modules/napalm_get_facts.py +++ b/napalm_ansible/modules/napalm_get_facts.py @@ -1,4 +1,5 @@ """ +(c) 2020 Kirk Byers (c) 2016 Elisa Jasinska This file is part of Ansible @@ -29,7 +30,7 @@ def return_values(obj): yield str(obj) -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: napalm_get_facts author: "Elisa Jasinska (@fooelisa)" @@ -95,9 +96,9 @@ def return_values(obj): the getter (same as the filter) required: False default: None -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - name: get facts from device napalm_get_facts: hostname: '{{ inventory_hostname }}' @@ -132,9 +133,9 @@ def return_values(obj): protocol: static destination: 8.8.8.8 -''' +""" -RETURN = ''' +RETURN = """ changed: description: "whether the command has been executed on the device" returned: always @@ -144,12 +145,13 @@ def return_values(obj): 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 @@ -158,61 +160,66 @@ def return_values(obj): 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']), - + 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 + supports_check_mode=True, ) if not napalm_found: module.fail_json(msg="the python module napalm is required") - provider = module.params['provider'] or {} + provider = module.params["provider"] or {} - no_log = ['password', 'secret'] + 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))) + 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) + 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'] + 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} + 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: + if module.params["optional_args"] is None: optional_args = {} else: - optional_args = module.params['optional_args'] + optional_args = module.params["optional_args"] try: network_driver = get_network_driver(dev_os) @@ -220,11 +227,13 @@ def main(): 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 = 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)) @@ -232,7 +241,11 @@ def main(): # retreive data from device facts = {} - NAPALM_GETTERS = [getter for getter in dir(network_driver) if getter.startswith("get_")] + 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) @@ -240,6 +253,8 @@ def main(): 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 @@ -249,9 +264,13 @@ def main(): else: module.fail_json( msg="The filter {} is not supported in napalm-{} [get_{}()]".format( - getter, dev_os, getter)) + getter, dev_os, getter + ) + ) except Exception as e: - module.fail_json(msg="[{}] cannot retrieve device data: ".format(getter) + str(e)) + module.fail_json( + msg="[{}] cannot retrieve device data: ".format(getter) + str(e) + ) # close device connection try: @@ -269,13 +288,13 @@ def main(): 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} + results = {"ansible_facts": new_facts} if ignore_notimplemented: - results['not_implemented'] = sorted(implementation_errors) + results["not_implemented"] = sorted(implementation_errors) module.exit_json(**results) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/napalm_ansible/modules/napalm_install_config.py b/napalm_ansible/modules/napalm_install_config.py index bfa9cef..8d6153e 100644 --- a/napalm_ansible/modules/napalm_install_config.py +++ b/napalm_ansible/modules/napalm_install_config.py @@ -1,4 +1,5 @@ """ +(c) 2020 Kirk Byers (c) 2016 Elisa Jasinska Original prototype by David Barroso @@ -31,7 +32,7 @@ def return_values(obj): yield str(obj) -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: napalm_install_config author: "Elisa Jasinska (@fooelisa)" @@ -58,10 +59,8 @@ def return_values(obj): 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'] + 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: @@ -120,14 +119,12 @@ def return_values(obj): required: False candidate_file: description: - - File to store backup of candidate config from device. This is the - config we are intending to install, before we roll back to the - running_config. + - Store a backup of candidate config from device prior to a commit. default: None required: False -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - assemble: src: '../compiled/{{ inventory_hostname }}/' dest: '../compiled/{{ inventory_hostname }}/running.conf' @@ -152,9 +149,9 @@ def return_values(obj): replace_config: '{{ replace_config }}' get_diffs: True diff_file: '../compiled/{{ inventory_hostname }}/diff' -''' +""" -RETURN = ''' +RETURN = """ changed: description: whether the config on the device was changed returned: always @@ -165,78 +162,85 @@ def return_values(obj): returned: always type: string sample: "[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: + 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) + 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 + supports_check_mode=True, ) if not napalm_found: module.fail_json(msg="the python module napalm is required") - provider = module.params['provider'] or {} + provider = module.params["provider"] or {} - no_log = ['password', 'secret'] + 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))) + 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) + 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'] + 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: @@ -246,15 +250,15 @@ def main(): if candidate_file: candidate_file = os.path.expanduser(os.path.expandvars(candidate_file)) - argument_check = {'hostname': hostname, 'username': username, 'dev_os': 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: + if module.params["optional_args"] is None: optional_args = {} else: - optional_args = module.params['optional_args'] + optional_args = module.params["optional_args"] try: network_driver = get_network_driver(dev_os) @@ -262,11 +266,13 @@ def main(): 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 = 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)) @@ -288,8 +294,7 @@ def main(): 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") + 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)) @@ -329,5 +334,5 @@ def main(): module.exit_json(changed=changed, msg=diff) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/napalm_ansible/modules/napalm_parse_yang.py b/napalm_ansible/modules/napalm_parse_yang.py index 9ba84c9..4bc939b 100644 --- a/napalm_ansible/modules/napalm_parse_yang.py +++ b/napalm_ansible/modules/napalm_parse_yang.py @@ -24,6 +24,7 @@ try: from napalm import get_network_driver from napalm.base import ModuleImportError + napalm_found = True except ImportError: pass @@ -43,7 +44,7 @@ def return_values(obj): yield str(obj) -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: napalm_parse_yang author: "David Barroso (@dbarrosop)" @@ -114,9 +115,9 @@ def return_values(obj): - A list profiles required: False choices: "" -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - name: Parse from device napalm_parse_yang: hostname: '{{ inventory_hostname }}' @@ -137,32 +138,37 @@ def return_values(obj): models: - models.openconfig_interfaces register: config -''' +""" -RETURN = ''' +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 {} + provider = module.params["provider"] or {} - no_log = ['password', 'secret'] + 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))) + 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) + 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: @@ -204,32 +210,31 @@ def parse_from_file(module): 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") + 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'] + 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} + 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: + if module.params["optional_args"] is None: optional_args = {} else: - optional_args = module.params['optional_args'] + optional_args = module.params["optional_args"] try: network_driver = get_network_driver(dev_os) @@ -237,11 +242,13 @@ def parse_from_device(module): 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 = 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)) @@ -266,21 +273,19 @@ def parse_from_device(module): 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"]), + 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), - + 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 + supports_check_mode=True, ) if not napalm_found: @@ -296,5 +301,5 @@ def main(): module.exit_json(yang_model=yang_model.to_dict(filter=True)) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/napalm_ansible/modules/napalm_ping.py b/napalm_ansible/modules/napalm_ping.py index 182a32f..de701c0 100644 --- a/napalm_ansible/modules/napalm_ping.py +++ b/napalm_ansible/modules/napalm_ping.py @@ -26,7 +26,7 @@ def return_values(obj): yield str(obj) -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: napalm_ping author: "Jason Edelman (@jedelman8)" @@ -93,9 +93,9 @@ def return_values(obj): description: vrf to source the echo request required: False -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - napalm_ping: hostname: "{{ inventory_hostname }}" username: "napalm" @@ -109,9 +109,9 @@ def return_values(obj): provider: "{{ napalm_provider }}" destination: 8.8.8.8 count: 2 -''' +""" -RETURN = ''' +RETURN = """ changed: description: ALWAYS RETURNS FALSE returned: always @@ -135,12 +135,13 @@ def return_values(obj): 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 @@ -149,70 +150,76 @@ def return_values(obj): 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), + 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 + supports_check_mode=True, ) if not napalm_found: module.fail_json(msg="the python module napalm is required") - provider = module.params['provider'] or {} + provider = module.params["provider"] or {} - no_log = ['password', 'secret'] + 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))) + 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) + 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'] + 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'] + 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') + 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} + 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: + if module.params["optional_args"] is None: optional_args = {} else: - optional_args = module.params['optional_args'] + optional_args = module.params["optional_args"] try: network_driver = get_network_driver(dev_os) @@ -220,11 +227,13 @@ def main(): 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 = 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)) @@ -239,5 +248,5 @@ def main(): module.exit_json(changed=False, ping_results=ping_response) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/napalm_ansible/modules/napalm_translate_yang.py b/napalm_ansible/modules/napalm_translate_yang.py index d8f72d9..c39f840 100644 --- a/napalm_ansible/modules/napalm_translate_yang.py +++ b/napalm_ansible/modules/napalm_translate_yang.py @@ -27,7 +27,7 @@ napalm_yang = None -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: napalm_translate_yang author: "David Barroso (@dbarrosop)" @@ -59,9 +59,9 @@ description: - When translating config, replace resulting config here required: False -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - name: "Translate config" napalm_translate_yang: data: "{{ interfaces.yang_model }}" @@ -69,15 +69,15 @@ models: - models.openconfig_interfaces register: config -''' +""" -RETURN = ''' +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): @@ -100,11 +100,11 @@ def main(): 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), + data=dict(type="dict", required=True), + merge=dict(type="dict", required=False), + replace=dict(type="dict", required=False), ), - supports_check_mode=True + supports_check_mode=True, ) if not napalm_yang: @@ -128,5 +128,5 @@ def main(): module.exit_json(config=config) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/napalm_ansible/modules/napalm_validate.py b/napalm_ansible/modules/napalm_validate.py index ae138e5..f4a9f9d 100644 --- a/napalm_ansible/modules/napalm_validate.py +++ b/napalm_ansible/modules/napalm_validate.py @@ -5,6 +5,7 @@ try: from napalm import get_network_driver from napalm.base import ModuleImportError + napalm_found = True except ImportError: pass @@ -24,7 +25,7 @@ def return_values(obj): yield str(obj) -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: napalm_validate author: Gabriele Gerbino (@GGabriele) @@ -83,9 +84,9 @@ def return_values(obj): description: - dict to load into the YANG object required: False -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - name: GET VALIDATION REPORT napalm_validate: username: "{{ un }}" @@ -120,9 +121,9 @@ def return_values(obj): - models.openconfig_interfaces validation_file: "validate.yaml" register: report -''' +""" -RETURN = ''' +RETURN = """ changed: description: check to see if a change was made on the device. returned: always @@ -132,46 +133,51 @@ def return_values(obj): description: validation report obtained via napalm. returned: always type: dict -''' +""" def get_compliance_report(module, device): - return device.compliance_report(module.params['validation_file']) + return device.compliance_report(module.params["validation_file"]) def get_device_instance(module): - provider = module.params['provider'] or {} + provider = module.params["provider"] or {} - no_log = ['password', 'secret'] + 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))) + 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) + 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'] + 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} + 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 {} + optional_args = module.params["optional_args"] or {} try: network_driver = get_network_driver(dev_os) @@ -179,11 +185,13 @@ def get_device_instance(module): 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 = 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))) @@ -209,17 +217,17 @@ 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), + 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 + supports_check_mode=False, ) if not napalm_found: module.fail_json(msg="the python module napalm is required") @@ -243,18 +251,17 @@ def main(): try: device.close() except Exception as err: - module.fail_json( - msg="cannot close device connection: {0}".format(str(err))) + module.fail_json(msg="cannot close device connection: {0}".format(str(err))) results = {} - results['compliance_report'] = compliance_report - if not compliance_report['complies']: + results["compliance_report"] = compliance_report + if not compliance_report["complies"]: msg = "Device does not comply with policy" - results['msg'] = msg + results["msg"] = msg module.fail_json(**results) module.exit_json(**results) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/napalm_ansible/plugins/action/napalm.py b/napalm_ansible/plugins/action/napalm.py index ac37522..1952b26 100644 --- a/napalm_ansible/plugins/action/napalm.py +++ b/napalm_ansible/plugins/action/napalm.py @@ -1,4 +1,5 @@ from __future__ import absolute_import, division, print_function, unicode_literals + __metaclass__ = type from ansible.plugins.action.normal import ActionModule as _ActionModule @@ -8,20 +9,26 @@ class ActionModule(_ActionModule): def run(self, tmp=None, task_vars=None): pc = self._play_context - if hasattr(pc, "connection_user"): # new in ansible 2.3 + 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)) - provider['username'] = provider.get('username', pc.connection_user) - provider['password'] = provider.get('password', pc.password) + 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) + provider["timeout"] = provider.get("timeout", 60) - if hasattr(pc, 'network_os'): - provider['dev_os'] = provider.get('dev_os', pc.network_os) + if hasattr(pc, "network_os"): + provider["dev_os"] = provider.get("dev_os", pc.network_os) - self._task.args['provider'] = provider + self._task.args["provider"] = provider result = super(ActionModule, self).run(tmp, task_vars) return result diff --git a/requirements-dev.txt b/requirements-dev.txt index 16f2c75..501e828 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,5 @@ -ansible +pylama==7.7.1 pytest --r requirements.txt \ No newline at end of file +black==18.9b0; python_version >= '3.6' +ansible +-r requirements.txt diff --git a/requirements.txt b/requirements.txt index d59b294..6460676 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ -napalm +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' six diff --git a/setup.py b/setup.py index 8c3abce..de4d5f2 100644 --- a/setup.py +++ b/setup.py @@ -6,29 +6,25 @@ setup( name="napalm-ansible", - version='1.0.0', + version="1.1.0", packages=find_packages(exclude=("test*", "library")), author="David Barroso, Kirk Byers, Mircea Ulinic", - author_email="dbarrosop@dravetech.com, ktbyers@twb-tech.com", + author_email="ktbyers@twb-tech.com", description="Network Automation and Programmability Abstraction Layer with Multivendor support", classifiers=[ - 'Topic :: Utilities', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Operating System :: POSIX :: Linux', - 'Operating System :: POSIX :: Linux', - 'Operating System :: MacOS', + "Topic :: Utilities", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Operating System :: POSIX :: Linux", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", ], url="https://github.com/napalm-automation/napalm-ansible", include_package_data=True, install_requires=reqs, - entry_points={ - 'console_scripts': [ - 'napalm-ansible=napalm_ansible:main', - ], - } + entry_points={"console_scripts": ["napalm-ansible=napalm_ansible:main"]}, ) diff --git a/tests/test_documentation.py b/tests/test_documentation.py index 30418cd..06d9524 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -5,8 +5,8 @@ from glob import glob from importlib import import_module -module_files = glob('napalm_ansible/modules/napalm_*.py') -modules = [module.split('.')[0].replace('/', '.') for module in module_files] +module_files = glob("napalm_ansible/modules/napalm_*.py") +modules = [module.split(".")[0].replace("/", ".") for module in module_files] @pytest.fixture(params=modules) @@ -17,28 +17,28 @@ def ansible_module(request): def test_module_documentation_exists(ansible_module): module = import_module(ansible_module) content = dir(module) - assert 'DOCUMENTATION' in content - assert 'EXAMPLES' in content - assert 'RETURN' in content + assert "DOCUMENTATION" in content + assert "EXAMPLES" in content + assert "RETURN" in content def test_module_documentation_format(ansible_module): module = import_module(ansible_module) docs = yaml.safe_load(module.DOCUMENTATION) - assert 'author' in docs.keys() - assert 'description' in docs.keys() - assert 'short_description' in docs.keys() - assert 'options' in docs.keys() - for param in docs['options']: - assert 'description' in docs['options'][param].keys() - assert 'required' in docs['options'][param].keys() + assert "author" in docs.keys() + assert "description" in docs.keys() + assert "short_description" in docs.keys() + assert "options" in docs.keys() + for param in docs["options"]: + assert "description" in docs["options"][param].keys() + assert "required" in docs["options"][param].keys() def test_module_examples_format(ansible_module): module = import_module(ansible_module) - module_name = ansible_module.replace('napalm_ansible.', '') + module_name = ansible_module.replace("napalm_ansible.", "") examples = yaml.safe_load(module.EXAMPLES) - params = yaml.safe_load(module.DOCUMENTATION)['options'].keys() + params = yaml.safe_load(module.DOCUMENTATION)["options"].keys() for example in examples: if module_name in example.keys(): for param in example[module_name]: @@ -52,17 +52,17 @@ def test_module_return_format(ansible_module): def test_build_docs(ansible_module): try: - os.mkdir('module_docs') + os.mkdir("module_docs") except Exception: pass module = import_module(ansible_module) content = {} - content['doc'] = yaml.safe_load(module.DOCUMENTATION) - content['examples'] = module.EXAMPLES - content['example_lines'] = module.EXAMPLES.split('\n') - content['return_values'] = yaml.safe_load(module.RETURN) - module_name = ansible_module.replace('napalm_ansible.', '').split('.')[-1] + content["doc"] = yaml.safe_load(module.DOCUMENTATION) + content["examples"] = module.EXAMPLES + content["example_lines"] = module.EXAMPLES.split("\n") + content["return_values"] = yaml.safe_load(module.RETURN) + module_name = ansible_module.replace("napalm_ansible.", "").split(".")[-1] - with open('module_docs/{0}.json'.format(module_name), 'w') as f: + with open("module_docs/{0}.json".format(module_name), "w") as f: json.dump(content, f, indent=4, sort_keys=False)