From 9f58e915d9bcb23b16152b541d5ad414dbdc09c6 Mon Sep 17 00:00:00 2001 From: Xu Yuandong Date: Tue, 26 Nov 2019 19:21:40 +0800 Subject: [PATCH] add a new module to configure mdn on interface. (#63755) * add a new module to confiure mdn on interface. * doc-default-does-not-match-spec. * add unittest * Update test_ce_mdn_interface.py * add integration. * update for review. * add notes. --- .../network/cloudengine/ce_mdn_interface.py | 403 ++++++++++++++++++ .../ce_mdn_interface/defaults/main.yaml | 3 + .../targets/ce_mdn_interface/tasks/main.yaml | 2 + .../ce_mdn_interface/tasks/netconf.yaml | 17 + .../tests/netconf/ce_mdn_interface.yaml | 97 +++++ .../fixtures/ce_mdn_interface/after.txt | 14 + .../fixtures/ce_mdn_interface/before.txt | 14 + .../cloudengine/test_ce_mdn_interface.py | 67 +++ 8 files changed, 617 insertions(+) create mode 100644 lib/ansible/modules/network/cloudengine/ce_mdn_interface.py create mode 100644 test/integration/targets/ce_mdn_interface/defaults/main.yaml create mode 100644 test/integration/targets/ce_mdn_interface/tasks/main.yaml create mode 100644 test/integration/targets/ce_mdn_interface/tasks/netconf.yaml create mode 100644 test/integration/targets/ce_mdn_interface/tests/netconf/ce_mdn_interface.yaml create mode 100644 test/units/modules/network/cloudengine/fixtures/ce_mdn_interface/after.txt create mode 100644 test/units/modules/network/cloudengine/fixtures/ce_mdn_interface/before.txt create mode 100644 test/units/modules/network/cloudengine/test_ce_mdn_interface.py diff --git a/lib/ansible/modules/network/cloudengine/ce_mdn_interface.py b/lib/ansible/modules/network/cloudengine/ce_mdn_interface.py new file mode 100644 index 0000000000..5b2d3203b8 --- /dev/null +++ b/lib/ansible/modules/network/cloudengine/ce_mdn_interface.py @@ -0,0 +1,403 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- + +module: ce_mdn_interface +version_added: "2.10" +short_description: Manages MDN configuration on HUAWEI CloudEngine switches. +description: + - Manages MDN configuration on HUAWEI CloudEngine switches. +author: xuxiaowei0512 (@CloudEngine-Ansible) +options: + lldpenable: + description: + - Set global LLDP enable state. + type: str + choices: ['enabled', 'disabled'] + mdnstatus: + description: + - Set interface MDN enable state. + type: str + choices: ['rxOnly', 'disabled'] + ifname: + description: + - Interface name. + type: str + state: + description: + - Manage the state of the resource. + default: present + type: str + choices: ['present','absent'] +notes: + - This module requires the netconf system service be enabled on + the remote device being managed. + - This module works with connection C(netconf). +''' + +EXAMPLES = ''' + - name: "Configure global LLDP enable state" + ce_mdn_interface: + lldpenable: enabled + + - name: "Configure interface MDN enable state" + ce_mdn_interface: + ifname: 10GE1/0/1 + mdnstatus: rxOnly +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "ifname": "10GE1/0/1", + "mdnstatus": "rxOnly", + "state":"present" + } +existing: + description: k/v pairs of existing global LLDP configration + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "ifname": "10GE1/0/1", + "mdnstatus": "disabled" + } +end_state: + description: k/v pairs of global LLDP configration after module execution + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "ifname": "10GE1/0/1", + "mdnstatus": "rxOnly" + } +updates: + description: command sent to the device + returned: always + type: list + sample: [ + "interface 10ge 1/0/1", + "lldp mdn enable", + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import copy +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.cloudengine.ce import set_nc_config, get_nc_config, execute_nc_action + +CE_NC_GET_GLOBAL_LLDPENABLE_CONFIG = """ + + + + + + + +""" + +CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG = """ + + + + %s + + + +""" + +CE_NC_GET_INTERFACE_MDNENABLE_CONFIG = """ + + + + + + + + + + +""" + +CE_NC_MERGE_INTERFACE_MDNENABLE_CONFIG = """ + + + + + %s + %s + + + + +""" + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('PORT-GROUP'): + iftype = 'stack-Port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + return iftype.lower() + + +class Interface_mdn(object): + """Manage global lldp enable configration""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # LLDP global configration info + self.lldpenable = self.module.params['lldpenable'] or None + self.ifname = self.module.params['ifname'] + self.mdnstatus = self.module.params['mdnstatus'] or None + self.state = self.module.params['state'] + self.lldp_conf = dict() + self.conf_exsit = False + self.enable_flag = 0 + self.check_params() + + # state + self.changed = False + self.proposed_changed = dict() + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def check_params(self): + """Check all input params""" + + if self.ifname: + intf_type = get_interface_type(self.ifname) + if not intf_type: + self.module.fail_json( + msg='Error: ifname name of %s ' + 'is error.' % self.ifname) + if (len(self.ifname) < 1) or (len(self.ifname) > 63): + self.module.fail_json( + msg='Error: Ifname length is beetween 1 and 63.') + + def init_module(self): + """Init module object""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def config_interface_mdn(self): + """Configure lldp enabled and interface mdn enabled parameters""" + + if self.state == 'present': + if self.enable_flag == 0 and self.lldpenable == 'enabled': + xml_str = CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG % self.lldpenable + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_ENABLE_CONFIG") + self.changed = True + elif self.enable_flag == 1 and self.lldpenable == 'disabled': + xml_str = CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG % self.lldpenable + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_ENABLE_CONFIG") + self.changed = True + elif self.enable_flag == 1 and self.conf_exsit: + xml_str = CE_NC_MERGE_INTERFACE_MDNENABLE_CONFIG % (self.ifname, self.mdnstatus) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "INTERFACE_MDN_ENABLE_CONFIG") + self.changed = True + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def get_interface_mdn_exist_config(self): + """Get lldp existed configure""" + + lldp_config = list() + lldp_dict = dict() + conf_enable_str = CE_NC_GET_GLOBAL_LLDPENABLE_CONFIG + conf_enable_obj = get_nc_config(self.module, conf_enable_str) + xml_enable_str = conf_enable_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get lldp enable config info + root_enable = ElementTree.fromstring(xml_enable_str) + ntpsite_enable = root_enable.findall("lldp/lldpSys") + for nexthop_enable in ntpsite_enable: + for ele_enable in nexthop_enable: + if ele_enable.tag in ["lldpEnable"]: + lldp_dict[ele_enable.tag] = ele_enable.text + + if self.state == "present": + if lldp_dict['lldpEnable'] == 'enabled': + self.enable_flag = 1 + lldp_config.append(dict(lldpenable=lldp_dict['lldpEnable'])) + + if self.enable_flag == 1: + conf_str = CE_NC_GET_INTERFACE_MDNENABLE_CONFIG + conf_obj = get_nc_config(self.module, conf_str) + if "" in conf_obj: + return lldp_config + xml_str = conf_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + # get all ntp config info + root = ElementTree.fromstring(xml_str) + ntpsite = root.findall("lldp/mdnInterfaces/mdnInterface") + for nexthop in ntpsite: + for ele in nexthop: + if ele.tag in ["ifName", "mdnStatus"]: + lldp_dict[ele.tag] = ele.text + if self.state == "present": + cur_interface_mdn_cfg = dict(ifname=lldp_dict['ifName'], mdnstatus=lldp_dict['mdnStatus']) + exp_interface_mdn_cfg = dict(ifname=self.ifname, mdnstatus=self.mdnstatus) + if self.ifname == lldp_dict['ifName']: + if cur_interface_mdn_cfg != exp_interface_mdn_cfg: + self.conf_exsit = True + lldp_config.append(dict(ifname=lldp_dict['ifName'], mdnstatus=lldp_dict['mdnStatus'])) + return lldp_config + lldp_config.append(dict(ifname=lldp_dict['ifName'], mdnstatus=lldp_dict['mdnStatus'])) + return lldp_config + + def get_existing(self): + """Get existing info""" + + self.existing = self.get_interface_mdn_exist_config() + + def get_proposed(self): + """Get proposed info""" + + if self.lldpenable: + self.proposed = dict(lldpenable=self.lldpenable) + if self.enable_flag == 1: + if self.ifname: + self.proposed = dict(ifname=self.ifname, mdnstatus=self.mdnstatus) + + def get_end_state(self): + """Get end state info""" + + self.end_state = self.get_interface_mdn_exist_config() + + def get_update_cmd(self): + """Get updated commands""" + + update_list = list() + if self.state == "present": + if self.lldpenable == "enabled": + cli_str = "lldp enable" + update_list.append(cli_str) + if self.ifname: + cli_str = "%s %s" % ("interface", self.ifname) + update_list.append(cli_str) + if self.mdnstatus: + if self.mdnstatus == "rxOnly": + cli_str = "lldp mdn enable" + update_list.append(cli_str) + else: + cli_str = "undo lldp mdn enable" + update_list.append(cli_str) + + elif self.lldpenable == "disabled": + cli_str = "undo lldp enable" + update_list.append(cli_str) + else: + if self.enable_flag == 1: + if self.ifname: + cli_str = "%s %s" % ("interface", self.ifname) + update_list.append(cli_str) + if self.mdnstatus: + if self.mdnstatus == "rxOnly": + cli_str = "lldp mdn enable" + update_list.append(cli_str) + else: + cli_str = "undo lldp mdn enable" + update_list.append(cli_str) + + self.updates_cmd.append(update_list) + + def work(self): + """Excute task""" + self.check_params() + self.get_existing() + self.get_proposed() + self.config_interface_mdn() + self.get_update_cmd() + self.get_end_state() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + lldpenable=dict(type='str', choices=['enabled', 'disabled']), + mdnstatus=dict(type='str', choices=['rxOnly', 'disabled']), + ifname=dict(type='str'), + state=dict(choices=['absent', 'present'], default='present'), + ) + lldp_obj = Interface_mdn(argument_spec) + lldp_obj.work() + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/ce_mdn_interface/defaults/main.yaml b/test/integration/targets/ce_mdn_interface/defaults/main.yaml new file mode 100644 index 0000000000..164afead28 --- /dev/null +++ b/test/integration/targets/ce_mdn_interface/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/ce_mdn_interface/tasks/main.yaml b/test/integration/targets/ce_mdn_interface/tasks/main.yaml new file mode 100644 index 0000000000..cc27f174fd --- /dev/null +++ b/test/integration/targets/ce_mdn_interface/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/ce_mdn_interface/tasks/netconf.yaml b/test/integration/targets/ce_mdn_interface/tasks/netconf.yaml new file mode 100644 index 0000000000..73b91adfaa --- /dev/null +++ b/test/integration/targets/ce_mdn_interface/tasks/netconf.yaml @@ -0,0 +1,17 @@ +--- +- name: collect all netconf test cases + find: + paths: "{{ role_path }}/tests/netconf" + patterns: "{{ testcase }}.yaml" + use_regex: true + connection: local + register: test_cases + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case (connection=netconf) + include: "{{ test_case_to_run }} ansible_connection=netconf" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/ce_mdn_interface/tests/netconf/ce_mdn_interface.yaml b/test/integration/targets/ce_mdn_interface/tests/netconf/ce_mdn_interface.yaml new file mode 100644 index 0000000000..4aec853fef --- /dev/null +++ b/test/integration/targets/ce_mdn_interface/tests/netconf/ce_mdn_interface.yaml @@ -0,0 +1,97 @@ +--- +- debug: + msg: "START ce_mdn_interface presented integration tests on connection={{ ansible_connection }}" +# set up default before test +- name: clean up default configuration with the exisiting running configuration + ce_mdn_interface: + lldpenable: disabled + mdnstatus: disabled + ifname: 10GE1/0/1 + +- name: present the provided configuration with the exisiting running configuration + ce_mdn_interface: &present + lldpenable: enabled + mdnstatus: rxOnly + ifname: 10GE1/0/1 + register: result + +- name: Assert the configuration is reflected on host + assert: + that: + - "result['changed'] == true" + +- name: Get mdnInterface config by ce_netconf. + ce_netconf: &get_config + rpc: get + cfg_xml: " + + + + 10GE1/0/1 + + + + + " + register: result_xml + +- name: Get lldp enabled config by ce_netconf. + ce_netconf: &get_config_lldp + rpc: get + cfg_xml: " + + + + + + /filter>" + register: result_xml_lldp + + +- name: present the provided configuration with the existing running configuration (IDEMPOTENT) + ce_mdn_interface: *present + register: repeat + +- name: Assert that the previous task was idempotent + assert: + that: + - "repeat.changed == false" + - "'rxOnly' in result_xml.end_state.result" + - "'enabled' in result_xml_lldp.end_state.result" + +- name: absent the provided configuration with the exisiting running configuration + ce_mdn_interface: &absent + lldpenable: disabled + mdnstatus: disabled + ifname: 10GE1/0/1 + state: absent + register: result + + +- name: Assert the configuration is reflected on host + assert: + that: + - "result['changed'] == true" + +- name: absent the provided configuration with the existing running configuration (REPEAT) + ce_mdn_interface: *absent + register: repeat + +- name: Get mdnInterface config by ce_netconf. + ce_netconf: *get_config + register: result_xml + +- name: Get lldp enabled config by ce_netconf. + ce_netconf: *get_config + register: result_xml_lldp + +- name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + - "'disabled' not in result_xml.end_state.result" + - "'disabled' in result_xml_lldp.end_state.result" +# after present, isis 100 should be deleted + +- debug: + msg: "END ce_mdn_interface resentd integration tests on connection={{ ansible_connection }}" diff --git a/test/units/modules/network/cloudengine/fixtures/ce_mdn_interface/after.txt b/test/units/modules/network/cloudengine/fixtures/ce_mdn_interface/after.txt new file mode 100644 index 0000000000..7cbc500df9 --- /dev/null +++ b/test/units/modules/network/cloudengine/fixtures/ce_mdn_interface/after.txt @@ -0,0 +1,14 @@ + + + + enabled + enabled + + + + 10GE1/0/1 + rxOnly + + + + diff --git a/test/units/modules/network/cloudengine/fixtures/ce_mdn_interface/before.txt b/test/units/modules/network/cloudengine/fixtures/ce_mdn_interface/before.txt new file mode 100644 index 0000000000..885dc9027b --- /dev/null +++ b/test/units/modules/network/cloudengine/fixtures/ce_mdn_interface/before.txt @@ -0,0 +1,14 @@ + + + + disabled + disabled + + + + 10GE1/0/1 + disabled + + + + diff --git a/test/units/modules/network/cloudengine/test_ce_mdn_interface.py b/test/units/modules/network/cloudengine/test_ce_mdn_interface.py new file mode 100644 index 0000000000..10e5e535da --- /dev/null +++ b/test/units/modules/network/cloudengine/test_ce_mdn_interface.py @@ -0,0 +1,67 @@ +# (c) 2019 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 units.compat.mock import patch +from ansible.modules.network.cloudengine import ce_mdn_interface +from units.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from units.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_mdn_interface + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible.modules.network.cloudengine.ce_mdn_interface.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible.modules.network.cloudengine.ce_mdn_interface.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = "" + self.before = load_fixture('ce_mdn_interface', 'before.txt') + self.after = load_fixture('ce_mdn_interface', 'after.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_mdn_enable(self): + update = [['lldp enable', 'interface 10GE1/0/1', 'lldp mdn enable']] + self.get_nc_config.side_effect = (self.before, self.before, self.after, self.after) + set_module_args(dict( + lldpenable='enabled', + mdnstatus='rxOnly', + ifname='10GE1/0/1') + ) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) + + def test_repeat_enable(self): + self.get_nc_config.side_effect = (self.after, self.after, self.after, self.after, ) + set_module_args(dict( + lldpenable='enabled', + mdnstatus='rxOnly', + ifname='10GE1/0/1') + ) + self.execute_module(changed=False)