diff --git a/lib/ansible/modules/network/cloudengine/ce_lacp.py b/lib/ansible/modules/network/cloudengine/ce_lacp.py new file mode 100644 index 00000000000..d488233b81e --- /dev/null +++ b/lib/ansible/modules/network/cloudengine/ce_lacp.py @@ -0,0 +1,488 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: ce_lacp +version_added: "2.10" +short_description: Manages Eth-Trunk interfaces on HUAWEI CloudEngine switches +description: + - Manages Eth-Trunk specific configuration parameters on HUAWEI CloudEngine switches. +author: xuxiaowei0512 (@CloudEngine-Ansible) +notes: + - C(state=absent) removes the Eth-Trunk config and interface if it + already exists. If members to be removed are not explicitly + passed, all existing members (if any), are removed, + and Eth-Trunk removed. + - Members must be a list. +options: + trunk_id: + description: + - Eth-Trunk interface number. + The value is an integer. + The value range depends on the assign forward eth-trunk mode command. + When 256 is specified, the value ranges from 0 to 255. + When 512 is specified, the value ranges from 0 to 511. + When 1024 is specified, the value ranges from 0 to 1023. + type: int + mode: + description: + - Specifies the working mode of an Eth-Trunk interface. + default: null + choices: ['Manual','Dynamic','Static'] + type: str + preempt_enable: + description: + - Specifies lacp preempt enable of Eth-Trunk lacp. + The value is an boolean 'true' or 'false'. + type: bool + state_flapping: + description: + - Lacp dampening state-flapping. + type: bool + port_id_extension_enable: + description: + - Enable the function of extending the LACP negotiation port number. + type: bool + unexpected_mac_disable: + description: + - Lacp dampening unexpected-mac disable. + type: bool + system_id: + description: + - Link Aggregation Control Protocol System ID,interface Eth-Trunk View. + - Formate 'X-X-X',X is hex(a,aa,aaa, or aaaa) + type: str + timeout_type: + description: + - Lacp timeout type,that may be 'Fast' or 'Slow'. + choices: ['Slow', 'Fast'] + type: str + fast_timeout: + description: + - When lacp timeout type is 'Fast', user-defined time can be a number(3~90). + type: int + mixed_rate_link_enable: + description: + - Value of max active linknumber. + type: bool + preempt_delay: + description: + - Value of preemption delay time. + type: int + collector_delay: + description: + - Value of delay time in units of 10 microseconds. + type: int + max_active_linknumber: + description: + - Max active linknumber in link aggregation group. + type: int + select: + description: + - Select priority or speed to preempt. + choices: ['Speed', 'Prority'] + type: str + priority: + description: + - The priority of eth-trunk member interface. + type: int + global_priority: + description: + - Configure lacp priority on system-view. + type: int + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] + type: str +''' +EXAMPLES = r''' + - name: Ensure Eth-Trunk100 is created, and set to mode lacp-static + ce_lacp: + trunk_id: 100 + mode: 'lacp-static' + state: present + - name: Ensure Eth-Trunk100 is created, add two members, and set global priority to 1231 + ce_lacp: + trunk_id: 100 + global_priority: 1231 + state: present + - name: Ensure Eth-Trunk100 is created, and set mode to Dynamic and configure other options + ce_lacp: + trunk_id: 100 + mode: Dynamic + preempt_enable: True, + state_flapping: True, + port_id_extension_enable: True, + unexpected_mac_disable: True, + timeout_type: Fast, + fast_timeout: 123, + mixed_rate_link_enable: True, + preempt_delay: 23, + collector_delay: 33, + state: present +''' + +RETURN = r''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"trunk_id": "100", "members": ['10GE1/0/24','10GE1/0/25'], "mode": "lacp-static"} +existing: + description: k/v pairs of existing Eth-Trunk + returned: always + type: dict + sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [ + {"memberIfName": "10GE1/0/25", "memberIfState": "Down"}], + "min_links": "1", "mode": "manual"} +end_state: + description: k/v pairs of Eth-Trunk info after module execution + returned: always + type: dict + sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [ + {"memberIfName": "10GE1/0/24", "memberIfState": "Down"}, + {"memberIfName": "10GE1/0/25", "memberIfState": "Down"}], + "min_links": "1", "mode": "lacp-static"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["interface Eth-Trunk 100", + "mode lacp-static", + "interface 10GE1/0/25", + "eth-trunk 100"] +''' + +import xml.etree.ElementTree as ET +import re +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config + +LACP = {'trunk_id': 'ifName', + 'mode': 'workMode', + 'preempt_enable': 'isSupportPrmpt', + 'state_flapping': 'dampStaFlapEn', + 'port_id_extension_enable': 'trunkPortIdExt', + 'unexpected_mac_disable': 'dampUnexpMacEn', + 'system_id': 'trunkSysMac', + 'timeout_type': 'rcvTimeoutType', + 'fast_timeout': 'fastTimeoutUserDefinedValue', + 'mixed_rate_link_enable': 'mixRateEnable', + 'preempt_delay': 'promptDelay', + 'collector_delay': 'collectMaxDelay', + 'max_active_linknumber': 'maxActiveNum', + 'select': 'selectPortStd', + 'weight': 'weight', + 'priority': 'portPriority', + 'global_priority': 'priority' + } + + +def has_element(parent, xpath): + """get or create a element by xpath""" + ele = parent.find('./' + xpath) + if ele is not None: + return ele + ele = parent + lpath = xpath.split('/') + for p in lpath: + e = parent.find('.//' + p) + if e is None: + e = ET.SubElement(ele, p) + ele = e + return ele + + +def bulid_xml(kwargs, operation='get'): + """create a xml tree by dictionary with operation,get,merge and delete""" + attrib = {'xmlns': "http://www.huawei.com/netconf/vrp", + 'content-version': "1.0", 'format-version': "1.0"} + + root = ET.Element('ifmtrunk') + for key in kwargs.keys(): + if key in ('global_priority',): + xpath = 'lacpSysInfo' + elif key in ('priority',): + xpath = 'TrunkIfs/TrunkIf/TrunkMemberIfs/TrunkMemberIf/lacpPortInfo/lacpPort' + elif key in ['preempt_enable', 'timeout_type', 'fast_timeout', 'select', 'preempt_delay', + 'max_active_linknumber', 'collector_delay', 'mixed_rate_link_enable', + 'state_flapping', 'unexpected_mac_disable', 'system_id', + 'port_id_extension_enable']: + xpath = 'TrunkIfs/TrunkIf/lacpTrunk' + elif key in ('trunk_id', 'mode'): + xpath = 'TrunkIfs/TrunkIf' + if xpath != '': + parent = has_element(root, xpath) + element = ET.SubElement(parent, LACP[key]) + if operation == 'merge': + parent.attrib = dict(operation=operation) + element.text = str(kwargs[key]) + if key == 'mode': + element.text = str(kwargs[key]) + if key == 'trunk_id': + element.text = 'Eth-Trunk' + str(kwargs[key]) + root.attrib = attrib + config = ET.tostring(root) + if operation == 'merge' or operation == 'delete': + return '%s' % to_native(config) + return '%s' % to_native(config) + + +def check_param(kwargs): + """check args list,the boolean or list values cloud not be checked,because they are limit by args list in main""" + + for key in kwargs: + if kwargs[key] is None: + continue + if key == 'trunk_id': + value = int(kwargs[key]) + # maximal value is 1024,although the value is limit by command 'assign forward eth-trunk mode ' + if value < 0 or value > 1024: + return 'Error: Wrong Value of Eth-Trunk interface number' + elif key == 'system_id': + # X-X-X ,X is hex(4 bit) + if not re.match(r'[0-9a-f]{1,4}\-[0-9a-f]{1,4}\-[0-9a-f]{1,4}', kwargs[key], re.IGNORECASE): + return 'Error: The system-id is invalid.' + values = kwargs[key].split('-') + flag = 0 + # all 'X' is 0,that is invalid value + for v in values: + if len(v.strip('0')) < 1: + flag += 1 + if flag == 3: + return 'Error: The system-id is invalid.' + elif key == 'timeout_type': + # select a value from choices, choices=['Slow','Fast'],it's checked by AnsibleModule + pass + elif key == 'fast_timeout': + value = int(kwargs[key]) + if value < 3 or value > 90: + return 'Error: Wrong Value of timeout,fast user-defined value<3-90>' + rtype = str(kwargs.get('timeout_type')) + if rtype == 'Slow': + return 'Error: Short timeout period for receiving packets is need,when user define the time.' + elif key == 'preempt_delay': + value = int(kwargs[key]) + if value < 0 or value > 180: + return 'Error: Value of preemption delay time is from 0 to 180' + elif key == 'collector_delay': + value = int(kwargs[key]) + if value < 0 or value > 65535: + return 'Error: Value of collector delay time is from 0 to 65535' + elif key == 'max_active_linknumber': + value = int(kwargs[key]) + if value < 0 or value > 64: + return 'Error: Value of collector delay time is from 0 to 64' + elif key == 'priority' or key == 'global_priority': + value = int(kwargs[key]) + if value < 0 or value > 65535: + return 'Error: Value of priority is from 0 to 65535' + return 'ok' + + +def xml_to_dict(args): + """transfer xml string into dict """ + rdict = dict() + args = re.sub(r'xmlns=\".+?\"', '', args) + root = ET.fromstring(args) + ifmtrunk = root.find('.//ifmtrunk') + if ifmtrunk is not None: + for ele in ifmtrunk.getiterator(): + if ele.text is not None and len(ele.text.strip()) > 0: + rdict[ele.tag] = ele.text + return rdict + + +def compare_config(module, kwarg_exist, kwarg_end): + """compare config between exist and end""" + dic_command = {'isSupportPrmpt': 'lacp preempt enable', + 'rcvTimeoutType': 'lacp timeout', # lacp timeout fast user-defined 23 + 'fastTimeoutUserDefinedValue': 'lacp timeout user-defined', + 'selectPortStd': 'lacp select', + 'promptDelay': 'lacp preempt delay', + 'maxActiveNum': 'lacp max active-linknumber', + 'collectMaxDelay': 'lacp collector delay', + 'mixRateEnable': 'lacp mixed-rate link enable', + 'dampStaFlapEn': 'lacp dampening state-flapping', + 'dampUnexpMacEn': 'lacp dampening unexpected-mac disable', + 'trunkSysMac': 'lacp system-id', + 'trunkPortIdExt': 'lacp port-id-extension enable', + 'portPriority': 'lacp priority', # interface 10GE1/0/1 + 'lacpMlagPriority': 'lacp m-lag priority', + 'lacpMlagSysId': 'lacp m-lag system-id', + 'priority': 'lacp priority' + } + rlist = list() + exist = set(kwarg_exist.keys()) + end = set(kwarg_end.keys()) + undo = exist - end + add = end - exist + update = end & exist + + for key in undo: + if key in dic_command: + rlist.append('undo ' + dic_command[key]) + for key in add: + if key in dic_command: + rlist.append(dic_command[key] + ' ' + kwarg_end[key]) + for key in update: + if kwarg_exist[key] != kwarg_end[key] and key in dic_command: + if kwarg_exist[key] == 'true' and kwarg_end[key] == 'false': + rlist.append('undo ' + dic_command[key]) + elif kwarg_exist[key] == 'false' and kwarg_end[key] == 'true': + rlist.append(dic_command[key]) + else: + rlist.append(dic_command[key] + ' ' + kwarg_end[key].lower()) + return rlist + + +class Lacp(object): + """ + Manages Eth-Trunk interfaces LACP. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.trunk_id = self.module.params['trunk_id'] + self.mode = self.module.params['mode'] + self.param = dict() + + self.state = self.module.params['state'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """ init AnsibleModule """ + + self.module = AnsibleModule( + argument_spec=self.spec, + mutually_exclusive=[['trunk_id', 'global_priority']], + required_one_of=[['trunk_id', 'global_priority']], + supports_check_mode=True) + + def check_params(self): + """check module params """ + for key in self.module.params.keys(): + if key in LACP.keys() and self.module.params[key] is not None: + self.param[key] = self.module.params[key] + if isinstance(self.module.params[key], bool): + self.param[key] = str(self.module.params[key]).lower() + msg = check_param(self.param) + if msg != 'ok': + self.module.fail_json(msg=msg) + + def get_existing(self): + """get existing""" + xml_str = bulid_xml(self.param) + xml = get_nc_config(self.module, xml_str) + return xml_to_dict(xml) + + def get_proposed(self): + """get proposed""" + proposed = dict(state=self.state) + proposed.update(self.param) + return proposed + + def get_end_state(self): + """ get end_state""" + xml_str = bulid_xml(self.param) + xml = get_nc_config(self.module, xml_str) + return xml_to_dict(xml) + + def work(self): + """worker""" + + self.check_params() + existing = self.get_existing() + proposed = self.get_proposed() + + # deal present or absent + if self.state == "present": + operation = 'merge' + else: + operation = 'delete' + + xml_str = bulid_xml(self.param, operation=operation) + set_nc_config(self.module, xml_str) + end_state = self.get_end_state() + + self.results['proposed'] = proposed + self.results['existing'] = existing + self.results['end_state'] = end_state + updates_cmd = compare_config(self.module, existing, end_state) + self.results['updates'] = updates_cmd + if updates_cmd: + self.results['changed'] = True + else: + self.results['changed'] = False + + self.module.exit_json(**self.results) + + +def main(): + + argument_spec = dict( + mode=dict(required=False, + choices=['Manual', 'Dynamic', 'Static'], + type='str'), + trunk_id=dict(required=False, type='int'), + preempt_enable=dict(required=False, type='bool'), + state_flapping=dict(required=False, type='bool'), + port_id_extension_enable=dict(required=False, type='bool'), + unexpected_mac_disable=dict(required=False, type='bool'), + system_id=dict(required=False, type='str'), + timeout_type=dict(required=False, type='str', choices=['Slow', 'Fast']), + fast_timeout=dict(required=False, type='int'), + mixed_rate_link_enable=dict(required=False, type='bool'), + preempt_delay=dict(required=False, type='int'), + collector_delay=dict(required=False, type='int'), + max_active_linknumber=dict(required=False, type='int'), + select=dict(required=False, type='str', choices=['Speed', 'Prority']), + priority=dict(required=False, type='int'), + global_priority=dict(required=False, type='int'), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + + module = Lacp(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/ce_lacp/defaults/main.yaml b/test/integration/targets/ce_lacp/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/ce_lacp/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/ce_lacp/tasks/main.yaml b/test/integration/targets/ce_lacp/tasks/main.yaml new file mode 100644 index 00000000000..cc27f174fd8 --- /dev/null +++ b/test/integration/targets/ce_lacp/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/ce_lacp/tasks/netconf.yaml b/test/integration/targets/ce_lacp/tasks/netconf.yaml new file mode 100644 index 00000000000..73b91adfaa2 --- /dev/null +++ b/test/integration/targets/ce_lacp/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_lacp/tests/netconf/absent.yaml b/test/integration/targets/ce_lacp/tests/netconf/absent.yaml new file mode 100644 index 00000000000..2c32e574d41 --- /dev/null +++ b/test/integration/targets/ce_lacp/tests/netconf/absent.yaml @@ -0,0 +1,95 @@ +--- +- debug: + msg: "START ce_lacp merged integration tests on connection={{ ansible_connection }}" +# befor removing, it should be merged +- include_tasks: merge.yaml + +- name: Merge the provided configuration with the exisiting running configuration + ce_lacp: &absent + mode: Dynamic + trunk_id: 10 + preempt_enable: True + state_flapping: True + port_id_extension_enable: True + unexpected_mac_disable: True + system_id: 1111-2222-3333 + timeout_type: Fast + fast_timeout: 12 + mixed_rate_link_enable: True + preempt_delay: 12 + collector_delay: 12 + max_active_linknumber: 2 + select: Prority + priority: 23 + global_priority: 123 + state: absent + register: result + +- name: Assert the configuration is reflected on host + assert: + that: + - "result['changed'] == true" + +- name: Get lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: " + + + + Eth-Trunk10 + + + + + + + + + + + + + + + + + + " + register: result_ifs_merged + +- name: Get lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: " + + + + + + " + register: result_global_merged + + +- name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) + ce_lacp: *absent + register: result_re_merged + +- name: Assert that the previous task was idempotent, some become ot default values, others depend on devices. + assert: + that: + - "result_re_merged.changed == false" + - "'false' == result_ifs_merged.end_state.result" + - "'Slow' == result_ifs_merged.end_state.result" + - "'90' == result_ifs_merged.end_state.result" + - "'Prority' == result_ifs_merged.end_state.result" + - "'30' == result_ifs_merged.end_state.result" + - "'0' in result_ifs_merged.end_state.result" + - "'false' in result_ifs_merged.end_state.result" + - "'false' in result_ifs_merged.end_state.result" + - "'false' in result_ifs_merged.end_state.result" + - "'false' in result_ifs_merged.end_state.result" + - "'32768' in result_global_merged.end_state.result" + +- debug: + msg: "END ce_lacp merged integration tests on connection={{ ansible_connection }}" diff --git a/test/integration/targets/ce_lacp/tests/netconf/delete.yaml b/test/integration/targets/ce_lacp/tests/netconf/delete.yaml new file mode 100644 index 00000000000..a3ce81723a9 --- /dev/null +++ b/test/integration/targets/ce_lacp/tests/netconf/delete.yaml @@ -0,0 +1,32 @@ +--- +- debug: + msg: "START ce_lacp deleted integration tests on connection={{ ansible_connection }}" + +- name: Merge the provided configuration with the exisiting running configuration + ce_lacp: + mode: Dynamic + trunk_id: 10 + preempt_enable: True + state_flapping: True + port_id_extension_enable: True + unexpected_mac_disable: True + system_id: 1111-2222-3333 + timeout_type: Fast + fast_timeout: 12 + mixed_rate_link_enable: True + preempt_delay: 12 + collector_delay: 12 + max_active_linknumber: 2 + select: Prority + priority: 23 + global_priority: 123 + state: absent + register: result + +- name: Assert the configuration is reflected on host + assert: + that: + - "result['changed'] == true" + +- debug: + msg: "END ce_lacp deleted integration tests on connection={{ ansible_connection }}" diff --git a/test/integration/targets/ce_lacp/tests/netconf/merge.yaml b/test/integration/targets/ce_lacp/tests/netconf/merge.yaml new file mode 100644 index 00000000000..eef3956eeb1 --- /dev/null +++ b/test/integration/targets/ce_lacp/tests/netconf/merge.yaml @@ -0,0 +1,31 @@ +--- +- debug: + msg: "START ce_lacp merged integration tests on connection={{ ansible_connection }}" + +- name: Merge the provided configuration with the exisiting running configuration + ce_lacp: + mode: Dynamic + trunk_id: 10 + preempt_enable: True + state_flapping: True + port_id_extension_enable: True + unexpected_mac_disable: True + system_id: 1111-2222-3333 + timeout_type: Fast + fast_timeout: 12 + mixed_rate_link_enable: True + preempt_delay: 12 + collector_delay: 12 + max_active_linknumber: 2 + select: Prority + priority: 23 + global_priority: 123 + register: result + +- name: Assert the configuration is reflected on host + assert: + that: + - "result['changed'] == true" + +- debug: + msg: "END ce_lacp merged integration tests on connection={{ ansible_connection }}" diff --git a/test/integration/targets/ce_lacp/tests/netconf/present.yaml b/test/integration/targets/ce_lacp/tests/netconf/present.yaml new file mode 100644 index 00000000000..ee696c9e1c0 --- /dev/null +++ b/test/integration/targets/ce_lacp/tests/netconf/present.yaml @@ -0,0 +1,103 @@ +--- +- debug: + msg: "START ce_lacp presented integration tests on connection={{ ansible_connection }}" + +- name: present the provided configuration with the exisiting running configuration + ce_lacp: &present + mode: Dynamic + trunk_id: 10 + preempt_enable: True + state_flapping: True + port_id_extension_enable: True + unexpected_mac_disable: True + system_id: 1111-2222-3333 + timeout_type: Fast + fast_timeout: 12 + mixed_rate_link_enable: True + preempt_delay: 12 + collector_delay: 12 + max_active_linknumber: 2 + select: Prority + priority: 23 + global_priority: 123 + register: result + +- name: Assert the configuration is reflected on host + assert: + that: + - "result['changed'] == true" + +- name: Get lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: " + + + + Eth-Trunk10 + + + + + + + + + + + + + + + + + + + " + register: result_ifs_presentd + +- name: Get global lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: " + + + + + + + + + + " + register: result_global_presentd + + +- name: present the provided configuration with the existing running configuration (IDEMPOTENT) + ce_lacp: *present + register: result_re_presentd + +- name: Assert that the previous task was idempotent + assert: + that: + - "result_re_presentd.changed == false" + - "'Dynamic' == result_ifs_presentd.end_state.result" + - "'true' == result_ifs_presentd.end_state.result" + - "'Fast' == result_ifs_presentd.end_state.result" + - "'12' == result_ifs_presentd.end_state.result" + - "'Prority' == result_ifs_presentd.end_state.result" + - "'12' == result_ifs_presentd.end_state.result" + - "'2' == result_ifs_presentd.end_state.result" + - "'12' in result_ifs_presentd.end_state.result" + - "'true' in result_ifs_presentd.end_state.result" + - "'true' in result_ifs_presentd.end_state.result" + - "'true' in result_ifs_presentd.end_state.result" + - "'true' in result_ifs_presentd.end_state.result" + - "'true' in result_ifs_presentd.end_state.result" + - "'1111-2222-3333' in result_global_presentd.end_state.result" + - "'123' in result_global_presentd.end_state.result" + +# after present, it should be deleted +- include_tasks: delete.yaml +- debug: + msg: "END ce_lacp presentd integration tests on connection={{ ansible_connection }}" diff --git a/test/units/modules/network/cloudengine/__init__.py b/test/units/modules/network/cloudengine/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/units/modules/network/cloudengine/ce_module.py b/test/units/modules/network/cloudengine/ce_module.py new file mode 100644 index 00000000000..d7990d41384 --- /dev/null +++ b/test/units/modules/network/cloudengine/ce_module.py @@ -0,0 +1,90 @@ +# Copyright (c) 2019 Red Hat +# +# This file is a 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 + +import os +import json +from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase + + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def load_fixture(module_name, name, device=''): + path = os.path.join(fixture_path, module_name, device, name) + if not os.path.exists(path): + path = os.path.join(fixture_path, module_name, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except Exception: + pass + + fixture_data[path] = data + return data + + +class TestCloudEngineModule(ModuleTestCase): + + def execute_module(self, failed=False, changed=False, commands=None, sort=True, defaults=False): + + self.load_fixtures(commands) + + if failed: + result = self.failed() + self.assertTrue(result['failed'], result) + else: + result = self.changed(changed) + self.assertEqual(result['changed'], changed, result) + + if commands is not None: + if sort: + self.assertEqual(sorted(commands), sorted(result['commands']), result['commands']) + else: + self.assertEqual(commands, result['commands'], result['commands']) + + return result + + def failed(self): + with self.assertRaises(AnsibleFailJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertTrue(result['failed'], result) + return result + + def changed(self, changed=False): + with self.assertRaises(AnsibleExitJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], changed, result) + return result + + def load_fixtures(self, commands=None): + pass diff --git a/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_00.txt b/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_00.txt new file mode 100644 index 00000000000..974c52c764a --- /dev/null +++ b/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_00.txt @@ -0,0 +1,26 @@ + + + + + + + Eth-Trunk10 + + false + Fast + 3 + Speed + 30 + 1 + 0 + false + false + false + 11-22-33 + false + + + + + + \ No newline at end of file diff --git a/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_01.txt b/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_01.txt new file mode 100644 index 00000000000..03b3f31e347 --- /dev/null +++ b/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_01.txt @@ -0,0 +1,26 @@ + + + + + + + Eth-Trunk10 + + true + Fast + 10 + Speed + 130 + 13 + 12 + true + true + true + 0000-1111-2222 + true + + + + + + \ No newline at end of file diff --git a/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_10.txt b/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_10.txt new file mode 100644 index 00000000000..6abbbbfbe46 --- /dev/null +++ b/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_10.txt @@ -0,0 +1,10 @@ + + + + + + 32768 + + + + \ No newline at end of file diff --git a/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_11.txt b/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_11.txt new file mode 100644 index 00000000000..22260aa1e77 --- /dev/null +++ b/test/units/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_11.txt @@ -0,0 +1,10 @@ + + + + + + 32769 + + + + \ No newline at end of file diff --git a/test/units/modules/network/cloudengine/test_ce_lacp.py b/test/units/modules/network/cloudengine/test_ce_lacp.py new file mode 100644 index 00000000000..36ad6664bd4 --- /dev/null +++ b/test/units/modules/network/cloudengine/test_ce_lacp.py @@ -0,0 +1,134 @@ +# (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_lacp +from units.modules.utils import set_module_args +from .ce_module import TestCloudEngineModule, load_fixture + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_lacp + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible.modules.network.cloudengine.ce_lacp.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible.modules.network.cloudengine.ce_lacp.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = None + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_lacp_eturnk_present(self): + xml_existing = load_fixture('ce_lacp', 'ce_lacp_00.txt') + xml_end_state = load_fixture('ce_lacp', 'ce_lacp_01.txt') + update = ['lacp max active-linknumber 13', + 'lacp dampening state-flapping', + 'lacp port-id-extension enable', + 'lacp collector delay 12', + 'lacp preempt enable', + 'lacp system-id 0000-1111-2222', + 'lacp mixed-rate link enable', + 'lacp preempt delay 130', + 'lacp timeout user-defined 10', + 'lacp dampening unexpected-mac disable'] + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + set_module_args(dict( + mode='Dynamic', + trunk_id='10', + preempt_enable='true', + state_flapping='true', + port_id_extension_enable='true', + unexpected_mac_disable='true', + system_id='0000-1111-2222', + timeout_type='Fast', + fast_timeout='10', + mixed_rate_link_enable='true', + preempt_delay=11, + collector_delay=12, + max_active_linknumber=13, + select='Speed', + state='present')) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) + + def test_lacp_eturnk_absent(self): + xml_existing = load_fixture('ce_lacp', 'ce_lacp_10.txt') + xml_end_state = load_fixture('ce_lacp', 'ce_lacp_00.txt') + default_values = ['undo lacp priority', + 'lacp timeout Fast', + 'lacp max active-linknumber 1', + 'lacp collector delay 0', + 'lacp preempt enable false', + 'lacp dampening state-flapping false', + 'lacp dampening unexpected-mac disable false', + 'lacp mixed-rate link enable false', + 'lacp port-id-extension enable false', + 'lacp preempt delay 30', + 'lacp select Speed', + 'lacp system-id 11-22-33', + 'lacp timeout user-defined 3'] + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + set_module_args(dict( + mode='Dynamic', + trunk_id='10', + preempt_enable='true', + state_flapping='true', + port_id_extension_enable='true', + unexpected_mac_disable='true', + system_id='0000-1111-2222', + timeout_type='Fast', + fast_timeout='10', + mixed_rate_link_enable='true', + preempt_delay=11, + collector_delay=12, + max_active_linknumber=13, + select='Speed', + state='absent' + )) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(default_values)) + + def test_lacp_global_present(self): + xml_existing = load_fixture('ce_lacp', 'ce_lacp_10.txt') + xml_end_state = load_fixture('ce_lacp', 'ce_lacp_11.txt') + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + set_module_args(dict(global_priority=32769, + state='present')) + result = self.execute_module(changed=True) + self.assertEquals(result['updates'], ['lacp priority 32769']) + + def test_lacp_global_absent(self): + xml_existing = load_fixture('ce_lacp', 'ce_lacp_11.txt') + xml_end_state = load_fixture('ce_lacp', 'ce_lacp_10.txt') + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + set_module_args(dict(global_priority=32769, + state='absent')) + result = self.execute_module(changed=True) + # excpect: lacp priority is set to default value(32768) + self.assertEquals(result['updates'], ['lacp priority 32768'])