From e2bed36d129e4b9c48ec93e3479c58be81b28dad Mon Sep 17 00:00:00 2001 From: Kedar K <4506537+kedarX@users.noreply.github.com> Date: Tue, 24 Oct 2017 08:49:23 +0530 Subject: [PATCH] - Adds iosxr_netconf module to configure netconf service on IOSXR (#31715) * - Adds iosxr_netconf module to configure netcong service on Cisco IOS-XR devices * - Adds Integration test for module - Handles diff return from load_config * - Adds unit test for iosxr_netconf module --- .../modules/network/iosxr/iosxr_netconf.py | 210 ++++++++++++++++++ .../targets/iosxr_netconf/defaults/main.yaml | 3 + .../targets/iosxr_netconf/meta/main.yaml | 2 + .../targets/iosxr_netconf/tasks/cli.yaml | 16 ++ .../targets/iosxr_netconf/tasks/main.yaml | 2 + .../iosxr_netconf/tests/cli/basic.yaml | 69 ++++++ .../network/iosxr/test_iosxr_netconf.py | 87 ++++++++ 7 files changed, 389 insertions(+) create mode 100644 lib/ansible/modules/network/iosxr/iosxr_netconf.py create mode 100644 test/integration/targets/iosxr_netconf/defaults/main.yaml create mode 100644 test/integration/targets/iosxr_netconf/meta/main.yaml create mode 100644 test/integration/targets/iosxr_netconf/tasks/cli.yaml create mode 100644 test/integration/targets/iosxr_netconf/tasks/main.yaml create mode 100644 test/integration/targets/iosxr_netconf/tests/cli/basic.yaml create mode 100644 test/units/modules/network/iosxr/test_iosxr_netconf.py diff --git a/lib/ansible/modules/network/iosxr/iosxr_netconf.py b/lib/ansible/modules/network/iosxr/iosxr_netconf.py new file mode 100644 index 0000000000..c3c8d01c18 --- /dev/null +++ b/lib/ansible/modules/network/iosxr/iosxr_netconf.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: iosxr_netconf +version_added: "2.5" +author: "Kedar Kekan (@kedarX)" +short_description: Configures NetConf sub-system service on Cisco IOS-XR devices +description: + - This module provides an abstraction that enables and configures + the netconf system service running on Cisco IOS-XR Software. + This module can be used to easily enable the Netconf API. Netconf provides + a programmatic interface for working with configuration and state + resources as defined in RFC 6242. +extends_documentation_fragment: iosxr +options: + netconf_port: + description: + - This argument specifies the port the netconf service should + listen on for SSH connections. The default port as defined + in RFC 6242 is 830. + required: false + default: 830 + aliases: ['listens_on'] + netconf_vrf: + description: + - netconf vrf name + required: false + default: none + state: + description: + - Specifies the state of the C(iosxr_netconf) resource on + the remote device. If the I(state) argument is set to + I(present) the netconf service will be configured. If the + I(state) argument is set to I(absent) the netconf service + will be removed from the configuration. + required: false + default: present + choices: ['present', 'absent'] +notes: + - Tested against Cisco IOS XR Software, Version 6.1.2 +""" + +EXAMPLES = """ +- name: enable netconf service on port 830 + iosxr_netconf: + listens_on: 830 + state: present + +- name: disable netconf service + iosxr_netconf: + state: absent +""" + +RETURN = """ +commands: + description: Returns the command sent to the remote device + returned: when changed is True + type: str + sample: 'ssh server netconf port 830' +""" +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import exec_command +from ansible.module_utils.iosxr import iosxr_argument_spec, check_args +from ansible.module_utils.iosxr import get_config, load_config +from ansible.module_utils.six import iteritems + +USE_PERSISTENT_CONNECTION = True + + +def map_obj_to_commands(updates, module): + want, have = updates + commands = list() + + if want['state'] == 'absent': + if have['state'] == 'present': + commands.append('no netconf-yang agent ssh') + + if 'netconf_port' in have: + commands.append('no ssh server netconf port %s' % have['netconf_port']) + + if want['netconf_vrf']: + for vrf in have['netconf_vrf']: + if vrf == want['netconf_vrf']: + commands.append('no ssh server netconf vrf %s' % vrf) + else: + for vrf in have['netconf_vrf']: + commands.append('no ssh server netconf vrf %s' % vrf) + else: + if have['state'] == 'absent': + commands.append('netconf-yang agent ssh') + + if want['netconf_port'] is not None and (want['netconf_port'] != have.get('netconf_port')): + commands.append( + 'ssh server netconf port %s' % want['netconf_port'] + ) + if want['netconf_vrf'] is not None and (want['netconf_vrf'] not in have['netconf_vrf']): + commands.append( + 'ssh server netconf vrf %s' % want['netconf_vrf'] + ) + + return commands + + +def parse_vrf(config): + match = re.search(r'vrf (\w+)', config) + if match: + return match.group(1) + + +def parse_port(config): + match = re.search(r'port (\d+)', config) + if match: + return int(match.group(1)) + + +def map_config_to_obj(module): + obj = {'state': 'absent'} + + netconf_config = get_config(module, flags=['netconf-yang agent']) + + ssh_config = get_config(module, flags=['ssh server']) + ssh_config = [config_line for config_line in (line.strip() for line in ssh_config.splitlines()) if config_line] + obj['netconf_vrf'] = [] + for config in ssh_config: + if 'netconf port' in config: + obj.update({'netconf_port': parse_port(config)}) + if 'netconf vrf' in config: + obj['netconf_vrf'].append(parse_vrf(config)) + if 'ssh' in netconf_config or 'netconf_port' in obj or obj['netconf_vrf']: + obj.update({'state': 'present'}) + + if 'ssh' in netconf_config and 'netconf_port' not in obj: + obj.update({'netconf_port': 830}) + + return obj + + +def validate_netconf_port(value, module): + if not 1 <= value <= 65535: + module.fail_json(msg='netconf_port must be between 1 and 65535') + + +def map_params_to_obj(module): + obj = { + 'netconf_port': module.params['netconf_port'], + 'netconf_vrf': module.params['netconf_vrf'], + 'state': module.params['state'] + } + + for key, value in iteritems(obj): + # validate the param value (if validator func exists) + validator = globals().get('validate_%s' % key) + if callable(validator): + validator(value, module) + + return obj + + +def main(): + """main entry point for module execution + """ + argument_spec = dict( + netconf_port=dict(type='int', default=830, aliases=['listens_on']), + netconf_vrf=dict(aliases=['vrf']), + state=dict(default='present', choices=['present', 'absent']), + ) + argument_spec.update(iosxr_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + warnings = list() + check_args(module, warnings) + + result = {'changed': False, 'warnings': warnings} + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + if not module.check_mode: + diff = load_config(module, commands, result['warnings'], commit=True) + if diff: + if module._diff: + result['diff'] = {'prepared': diff} + exec_command(module, 'exit') + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/iosxr_netconf/defaults/main.yaml b/test/integration/targets/iosxr_netconf/defaults/main.yaml new file mode 100644 index 0000000000..9ef5ba5165 --- /dev/null +++ b/test/integration/targets/iosxr_netconf/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "*" +test_items: [] diff --git a/test/integration/targets/iosxr_netconf/meta/main.yaml b/test/integration/targets/iosxr_netconf/meta/main.yaml new file mode 100644 index 0000000000..d4da833dd5 --- /dev/null +++ b/test/integration/targets/iosxr_netconf/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_iosxr_tests diff --git a/test/integration/targets/iosxr_netconf/tasks/cli.yaml b/test/integration/targets/iosxr_netconf/tasks/cli.yaml new file mode 100644 index 0000000000..46d86dd698 --- /dev/null +++ b/test/integration/targets/iosxr_netconf/tasks/cli.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/iosxr_netconf/tasks/main.yaml b/test/integration/targets/iosxr_netconf/tasks/main.yaml new file mode 100644 index 0000000000..415c99d8b1 --- /dev/null +++ b/test/integration/targets/iosxr_netconf/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/iosxr_netconf/tests/cli/basic.yaml b/test/integration/targets/iosxr_netconf/tests/cli/basic.yaml new file mode 100644 index 0000000000..43af3aacf8 --- /dev/null +++ b/test/integration/targets/iosxr_netconf/tests/cli/basic.yaml @@ -0,0 +1,69 @@ +--- +- debug: msg="START iosxr_netconf cli/basic.yaml" + +- name: Disable NetConf service + iosxr_netconf: &disable_netconf + state: absent + +- block: + - name: Enable Netconf service + iosxr_netconf: + netconf_port: 830 + netconf_vrf: 'default' + state: present + register: result + + - assert: &true + that: + - 'result.changed == true' + + - name: Check idempotence of Enable Netconf service + iosxr_netconf: + netconf_port: 830 + netconf_vrf: 'default' + state: present + register: result + + - assert: &false + that: + - 'result.changed == false' + + - name: Change Netconf port + iosxr_netconf: + netconf_port: 9000 + state: present + register: result + + - assert: *true + + - name: Check idempotent of change Netconf port + iosxr_netconf: + netconf_port: 9000 + state: present + register: result + + - assert: *false + + - name: Add Netconf vrf + iosxr_netconf: + netconf_port: 9000 + netconf_vrf: 'new_default' + state: present + register: result + + - assert: *true + + - name: Check idempotent of add Netconf vrf + iosxr_netconf: + netconf_port: 9000 + netconf_vrf: 'new_default' + state: present + register: result + + - assert: *false + + always: + - name: Disable Netconf service + iosxr_netconf: *disable_netconf + +- debug: msg="END iosxr_netconf cli/basic.yaml" diff --git a/test/units/modules/network/iosxr/test_iosxr_netconf.py b/test/units/modules/network/iosxr/test_iosxr_netconf.py new file mode 100644 index 0000000000..544059d01c --- /dev/null +++ b/test/units/modules/network/iosxr/test_iosxr_netconf.py @@ -0,0 +1,87 @@ +# (c) 2017 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.compat.tests.mock import patch +from ansible.modules.network.iosxr import iosxr_netconf +from .iosxr_module import TestIosxrModule, set_module_args + + +class TestIosxrNetconfModule(TestIosxrModule): + + module = iosxr_netconf + + def setUp(self): + self.mock_exec_command = patch('ansible.modules.network.iosxr.iosxr_netconf.exec_command') + self.exec_command = self.mock_exec_command.start() + + self.mock_get_config = patch('ansible.modules.network.iosxr.iosxr_netconf.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch('ansible.modules.network.iosxr.iosxr_netconf.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + self.mock_exec_command.stop() + self.mock_get_config.stop() + self.mock_load_config.stop() + + def test_iosxr_disable_netconf_service(self): + self.get_config.return_value = ''' + netconf-yang agent + ssh + ! + ssh server netconf vrf default + ''' + self.exec_command.return_value = 0, '', None + set_module_args(dict(netconf_port=830, netconf_vrf='default', state='absent')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['no netconf-yang agent ssh', 'no ssh server netconf port 830', 'no ssh server netconf vrf default']) + + def test_iosxr_enable_netconf_service(self): + self.get_config.return_value = '' + self.exec_command.return_value = 0, '', None + set_module_args(dict(netconf_port=830, netconf_vrf='default', state='present')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['netconf-yang agent ssh', 'ssh server netconf port 830', 'ssh server netconf vrf default']) + + def test_iosxr_change_netconf_port(self): + self.get_config.return_value = ''' + netconf-yang agent + ssh + ! + ssh server netconf vrf default + ''' + self.exec_command.return_value = 0, '', None + set_module_args(dict(netconf_port=9000, state='present')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['ssh server netconf port 9000']) + + def test_iosxr_change_netconf_vrf(self): + self.get_config.return_value = ''' + netconf-yang agent + ssh + ! + ssh server netconf vrf default + ''' + self.exec_command.return_value = 0, '', None + set_module_args(dict(netconf_vrf='new_default', state='present')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['ssh server netconf vrf new_default'])