From 8c88ee3b9f22f45a6551b75de9043c9219ef4ae5 Mon Sep 17 00:00:00 2001 From: Xu Yuandong Date: Tue, 26 Nov 2019 19:22:39 +0800 Subject: [PATCH] add a new module to configure multicast global. (#63745) * add a new module. * add future * update docs. * update * update * update * update * add test. * Update test_ce_multicast_global.py * update for review. * update for review. * add notes. --- .../cloudengine/ce_multicast_global.py | 290 ++++++++++++++++++ .../ce_multicast_global/defaults/main.yaml | 3 + .../ce_multicast_global/tasks/main.yaml | 2 + .../ce_multicast_global/tasks/netconf.yaml | 17 + .../netconf/test_ce_multicast_global.yaml | 73 +++++ .../fixtures/ce_multicast_global/after.txt | 10 + .../fixtures/ce_multicast_global/before.txt | 1 + .../cloudengine/test_ce_multicast_global.py | 69 +++++ 8 files changed, 465 insertions(+) create mode 100644 lib/ansible/modules/network/cloudengine/ce_multicast_global.py create mode 100644 test/integration/targets/ce_multicast_global/defaults/main.yaml create mode 100644 test/integration/targets/ce_multicast_global/tasks/main.yaml create mode 100644 test/integration/targets/ce_multicast_global/tasks/netconf.yaml create mode 100644 test/integration/targets/ce_multicast_global/tests/netconf/test_ce_multicast_global.yaml create mode 100644 test/units/modules/network/cloudengine/fixtures/ce_multicast_global/after.txt create mode 100644 test/units/modules/network/cloudengine/fixtures/ce_multicast_global/before.txt create mode 100644 test/units/modules/network/cloudengine/test_ce_multicast_global.py diff --git a/lib/ansible/modules/network/cloudengine/ce_multicast_global.py b/lib/ansible/modules/network/cloudengine/ce_multicast_global.py new file mode 100644 index 00000000000..b6b1af26a46 --- /dev/null +++ b/lib/ansible/modules/network/cloudengine/ce_multicast_global.py @@ -0,0 +1,290 @@ +#!/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_multicast_global +version_added: "2.10" +author: xuxiaowei0512 (@xuxiaowei0512) +short_description: Manages multicast global configuration on HUAWEI CloudEngine switches. +description: + - Manages multicast global on HUAWEI CloudEngine switches. +notes: + - If no vrf is supplied, vrf is set to default. + - If I(state=absent), the route will be removed, regardless of the non-required parameters. + - This module requires the netconf system service be enabled on the remote device being managed. + - This module works with connection C(netconf). +options: + aftype: + description: + - Destination ip address family type of static route. + required: true + type: str + choices: ['v4','v6'] + vrf: + description: + - VPN instance of destination ip address. + type: str + state: + description: + - Specify desired state of the resource. + type: str + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +--- + - name: multicast routing-enable + ce_multicast_global: + aftype: v4 + state: absent + provider: "{{ cli }}" + - name: multicast routing-enable + ce_multicast_global: + aftype: v4 + state: present + provider: "{{ cli }}" + - name: multicast routing-enable + ce_multicast_global: + aftype: v4 + vrf: vrf1 + provider: "{{ cli }}" + +''' +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"addressFamily": "ipv4unicast", "state": "present", "vrfName": "_public_"} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: {"addressFamily": "ipv4unicast", "state": "present", "vrfName": "_public_"} +updates: + description: command list sent to the device + returned: always + type: list + sample: ["multicast routing-enable"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config + +CE_NC_GET_MULTICAST_GLOBAL = """ + + + + + %s + %s + + + + +""" +CE_NC_MERGE_MULTICAST_GLOBAL = """ + + + + %s + %s + + + +""" +CE_NC_DELETE_MULTICAST_GLOBAL = """ + + + + %s + %s + + + +""" + + +def build_config_xml(xmlstr): + """build config xml""" + + return ' ' + xmlstr + ' ' + + +class MulticastGlobal(object): + """multicast global module""" + + def __init__(self, argument_spec): + """multicast global info""" + self.spec = argument_spec + self.module = None + self._initmodule_() + + self.aftype = self.module.params['aftype'] + self.state = self.module.params['state'] + if self.aftype == "v4": + self.version = "ipv4unicast" + else: + self.version = "ipv6unicast" + # vpn instance info + self.vrf = self.module.params['vrf'] + if self.vrf is None: + self.vrf = "_public_" + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + self.multicast_global_info = dict() + + def _initmodule_(self): + """init module""" + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=False) + + def _checkresponse_(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 set_change_state(self): + """set change state""" + state = self.state + change = False + self.get_multicast_global() + # new or edit + if state == 'present': + if not self.multicast_global_info.get('multicast_global'): + # i.e. self.multicast_global_info['multicast_global'] has not value + change = True + else: + # delete + if self.multicast_global_info.get('multicast_global'): + # i.e. self.multicast_global_info['multicast_global'] has value + change = True + self.changed = change + + def get_multicast_global(self): + """get one data""" + self.multicast_global_info["multicast_global"] = list() + getxmlstr = CE_NC_GET_MULTICAST_GLOBAL % ( + self.version, self.vrf) + xml_str = get_nc_config(self.module, getxmlstr) + if 'data/' in xml_str: + return + xml_str = xml_str.replace('\r', '').replace('\n', ''). \ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', ""). \ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + mcast_enable = root.findall( + "mcastbase/mcastAfsEnables/mcastAfsEnable") + if mcast_enable: + # i.e. mcast_enable = [{vrfName:11,addressFamily:'xx'},{vrfName:22,addressFamily:'xx'}...] + for mcast_enable_key in mcast_enable: + # i.e. mcast_enable_key = {vrfName:11,addressFamily:'xx'} + mcast_info = dict() + for ele in mcast_enable_key: + if ele.tag in ["vrfName", "addressFamily"]: + mcast_info[ele.tag] = ele.text + self.multicast_global_info['multicast_global'].append(mcast_info) + + def get_existing(self): + """get existing information""" + self.set_change_state() + self.existing["multicast_global"] = self.multicast_global_info["multicast_global"] + + def get_proposed(self): + """get proposed information""" + self.proposed['addressFamily'] = self.version + self.proposed['state'] = self.state + self.proposed['vrfName'] = self.vrf + + def set_multicast_global(self): + """set multicast global""" + if not self.changed: + return + version = self.version + state = self.state + if state == "present": + configxmlstr = CE_NC_MERGE_MULTICAST_GLOBAL % (self.vrf, version) + else: + configxmlstr = CE_NC_DELETE_MULTICAST_GLOBAL % (self.vrf, version) + + conf_str = build_config_xml(configxmlstr) + recv_xml = set_nc_config(self.module, conf_str) + self._checkresponse_(recv_xml, "SET_MULTICAST_GLOBAL") + + def set_update_cmd(self): + """set update command""" + if not self.changed: + return + if self.state == "present": + self.updates_cmd.append('multicast routing-enable') + else: + self.updates_cmd.append('undo multicast routing-enable') + + def get_end_state(self): + """get end state information""" + self.get_multicast_global() + self.end_state["multicast_global"] = self.multicast_global_info["multicast_global"] + + def work(self): + """worker""" + self.get_existing() + self.get_proposed() + self.set_multicast_global() + self.set_update_cmd() + self.get_end_state() + self.results['changed'] = self.changed + self.results['existing'] = self.existing + self.results['proposed'] = self.proposed + 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 main(): + """main""" + + argument_spec = dict( + aftype=dict(choices=['v4', 'v6'], required=True), + vrf=dict(required=False, type='str'), + state=dict(choices=['absent', 'present'], default='present', required=False), + ) + interface = MulticastGlobal(argument_spec) + interface.work() + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/ce_multicast_global/defaults/main.yaml b/test/integration/targets/ce_multicast_global/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/ce_multicast_global/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/ce_multicast_global/tasks/main.yaml b/test/integration/targets/ce_multicast_global/tasks/main.yaml new file mode 100644 index 00000000000..cc27f174fd8 --- /dev/null +++ b/test/integration/targets/ce_multicast_global/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/ce_multicast_global/tasks/netconf.yaml b/test/integration/targets/ce_multicast_global/tasks/netconf.yaml new file mode 100644 index 00000000000..73b91adfaa2 --- /dev/null +++ b/test/integration/targets/ce_multicast_global/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_multicast_global/tests/netconf/test_ce_multicast_global.yaml b/test/integration/targets/ce_multicast_global/tests/netconf/test_ce_multicast_global.yaml new file mode 100644 index 00000000000..69b1f5f26bc --- /dev/null +++ b/test/integration/targets/ce_multicast_global/tests/netconf/test_ce_multicast_global.yaml @@ -0,0 +1,73 @@ +--- +- debug: + msg: "START ce_multicast_global presented integration tests on connection={{ ansible_connection }}" + +- name: present the provided configuration with the exisiting running configuration + ce_multicast_global: &present + aftype: v4 + vrf: vpna + weight: 100 + register: result + +- name: Assert the configuration is reflected on host + assert: + that: + - "result['changed'] == true" + +- name: Get basic config by ce_netconf. + ce_netconf: &get_config + rpc: get + cfg_xml: " + + + + + + + + + " + register: result_xml + + +- name: present the provided configuration with the existing running configuration (IDEMPOTENT) + ce_multicast_global: *present + register: repeat + +- name: Assert that the previous task was idempotent + assert: + that: + - "repeat.changed == false" + - "'vpna' in result_xml.end_state.result" + - "'vpna' in result_xml.end_state.result" + +- name: present the provided configuration with the exisiting running configuration + ce_multicast_global: &absent + aftype: v4 + vrf: vpna + register: result + + +- name: Assert the configuration is reflected on host + assert: + that: + - "result['changed'] == true" + +- name: present the provided configuration with the existing running configuration (IDEMPOTENT) + ce_multicast_global: *absent + register: repeat + +- name: Get basic config by ce_netconf. + ce_netconf: *get_config + register: result_xml + +- name: Assert that the previous task was idempotent + assert: + that: + - "repeat.changed == false" + - "'vpna' not in result_xml.end_state.result" + - "'vpna' not in result_xml.end_state.result" +# after present, isis 100 should be deleted + +- debug: + msg: "END ce_multicast_global resentd integration tests on connection={{ ansible_connection }}" diff --git a/test/units/modules/network/cloudengine/fixtures/ce_multicast_global/after.txt b/test/units/modules/network/cloudengine/fixtures/ce_multicast_global/after.txt new file mode 100644 index 00000000000..b196031e7b1 --- /dev/null +++ b/test/units/modules/network/cloudengine/fixtures/ce_multicast_global/after.txt @@ -0,0 +1,10 @@ + + + + + vpna + ipv4unicast + + + + diff --git a/test/units/modules/network/cloudengine/fixtures/ce_multicast_global/before.txt b/test/units/modules/network/cloudengine/fixtures/ce_multicast_global/before.txt new file mode 100644 index 00000000000..fe6c8395715 --- /dev/null +++ b/test/units/modules/network/cloudengine/fixtures/ce_multicast_global/before.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/units/modules/network/cloudengine/test_ce_multicast_global.py b/test/units/modules/network/cloudengine/test_ce_multicast_global.py new file mode 100644 index 00000000000..5cc9f5ead3d --- /dev/null +++ b/test/units/modules/network/cloudengine/test_ce_multicast_global.py @@ -0,0 +1,69 @@ +# (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_multicast_global +from units.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from units.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_multicast_global + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible.modules.network.cloudengine.ce_multicast_global.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible.modules.network.cloudengine.ce_multicast_global.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = "" + self.before = load_fixture('ce_multicast_global', 'before.txt') + self.after = load_fixture('ce_multicast_global', 'after.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_multicast_enable(self): + update = ['multicast routing-enable'] + self.get_nc_config.side_effect = (self.before, self.after) + set_module_args(dict( + aftype='v4', + vrf='vpna', + state='present') + ) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) + + def test_multicast_undo_enable(self): + update = ['undo multicast routing-enable'] + self.get_nc_config.side_effect = (self.after, self.before) + set_module_args(dict( + aftype='v4', + vrf='vpna', + state='absent') + ) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update))