diff --git a/lib/ansible/modules/cloud/vmware/vmware_dvswitch_lacp.py b/lib/ansible/modules/cloud/vmware/vmware_dvswitch_lacp.py new file mode 100644 index 0000000000..70db65dcf5 --- /dev/null +++ b/lib/ansible/modules/cloud/vmware/vmware_dvswitch_lacp.py @@ -0,0 +1,403 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, Christian Kotte +# +# 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: vmware_dvswitch_lacp +short_description: Manage LACP configuration on a Distributed Switch +description: + - This module can be used to configure Link Aggregation Control Protocol (LACP) support mode and Link Aggregation Groups (LAGs). +version_added: 2.8 +author: +- Christian Kotte (@ckotte) +notes: + - Tested on vSphere 6.7 + - You need to run the task two times if you want to remove all LAGs and change the support mode to 'basic' +requirements: + - "python >= 2.6" + - PyVmomi +options: + switch: + description: + - The name of the Distributed Switch to manage. + required: True + aliases: ['dvswitch'] + support_mode: + description: + - The LACP support mode. + - 'C(basic): One Link Aggregation Control Protocol group in the switch (singleLag).' + - 'C(enhanced): Multiple Link Aggregation Control Protocol groups in the switch (multipleLag).' + type: str + default: 'basic' + choices: ['basic', 'enhanced'] + link_aggregation_groups: + description: + - Can only be used if C(lacp_support) is set to C(enhanced). + - 'The following parameters are required:' + - '- C(name) (string): Name of the LAG.' + - '- C(uplink_number) (int): Number of uplinks. Can 1 to 30.' + - '- C(mode) (string): The negotiating state of the uplinks/ports.' + - ' - choices: [ active, passive ]' + - '- C(load_balancing_mode) (string): Load balancing algorithm.' + - ' - Valid attributes are:' + - ' - srcTcpUdpPort: Source TCP/UDP port number.' + - ' - srcDestIpTcpUdpPortVlan: Source and destination IP, source and destination TCP/UDP port number and VLAN.' + - ' - srcIpVlan: Source IP and VLAN.' + - ' - srcDestTcpUdpPort: Source and destination TCP/UDP port number.' + - ' - srcMac: Source MAC address.' + - ' - destIp: Destination IP.' + - ' - destMac: Destination MAC address.' + - ' - vlan: VLAN only.' + - ' - srcDestIp: Source and Destination IP.' + - ' - srcIpTcpUdpPortVlan: Source IP, TCP/UDP port number and VLAN.' + - ' - srcDestIpTcpUdpPort: Source and destination IP and TCP/UDP port number.' + - ' - srcDestMac: Source and destination MAC address.' + - ' - destIpTcpUdpPort: Destination IP and TCP/UDP port number.' + - ' - srcPortId: Source Virtual Port Id.' + - ' - srcIp: Source IP.' + - ' - srcIpTcpUdpPort: Source IP and TCP/UDP port number.' + - ' - destIpTcpUdpPortVlan: Destination IP, TCP/UDP port number and VLAN.' + - ' - destTcpUdpPort: Destination TCP/UDP port number.' + - ' - destIpVlan: Destination IP and VLAN.' + - ' - srcDestIpVlan: Source and destination IP and VLAN.' + - ' - The default load balancing mode in the vSphere Client is srcDestIpTcpUdpPortVlan.' + - Please see examples for more information. + type: list +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' +- name: Enable enhanced mode on a Distributed Switch + vmware_dvswitch_lacp: + hostname: '{{ inventory_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + switch: dvSwitch + support_mode: enhanced + validate_certs: "{{ validate_vcenter_certs }}" + delegate_to: localhost + loop_control: + label: "{{ item.name }}" + with_items: "{{ vcenter_distributed_switches }}" + +- name: Enable enhanced mode and create two LAGs on a Distributed Switch + vmware_dvswitch_lacp: + hostname: '{{ inventory_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + switch: dvSwitch + support_mode: enhanced + link_aggregation_groups: + - name: lag1 + uplink_number: 2 + mode: active + load_balancing_mode: srcDestIpTcpUdpPortVlan + - name: lag2 + uplink_number: 2 + mode: passive + load_balancing_mode: srcDestIp + validate_certs: "{{ validate_vcenter_certs }}" + delegate_to: localhost + loop_control: + label: "{{ item.name }}" + with_items: "{{ vcenter_distributed_switches }}" +''' + +RETURN = """ +result: + description: information about performed operation + returned: always + type: string + sample: { + "changed": true, + "dvswitch": "dvSwitch", + "link_aggregation_groups": [ + {"load_balancing_mode": "srcDestIpTcpUdpPortVlan", "mode": "active", "name": "lag1", "uplink_number": 2}, + {"load_balancing_mode": "srcDestIp", "mode": "active", "name": "lag2", "uplink_number": 2} + ], + "link_aggregation_groups_previous": [], + "support_mode": "enhanced", + "result": "lacp lags changed" + } +""" + +try: + from pyVmomi import vim, vmodl +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible.module_utils.vmware import ( + PyVmomi, TaskError, find_dvs_by_name, vmware_argument_spec, wait_for_task +) + + +class VMwareDvSwitchLacp(PyVmomi): + """Class to manage a LACP on a Distributed Virtual Switch""" + def __init__(self, module): + super(VMwareDvSwitchLacp, self).__init__(module) + self.switch_name = self.module.params['switch'] + self.support_mode = self.module.params['support_mode'] + self.link_aggregation_groups = self.module.params['link_aggregation_groups'] + if self.support_mode == 'basic' and ( + self.link_aggregation_groups and not ( + len(self.link_aggregation_groups) == 1 and self.link_aggregation_groups[0] == '')): + self.module.fail_json( + msg="LAGs can only be configured if 'support_mode' is set to 'enhanced'!" + ) + self.dvs = find_dvs_by_name(self.content, self.switch_name) + if self.dvs is None: + self.module.fail_json(msg="Failed to find DVS %s" % self.switch_name) + + def ensure(self): + """Manage LACP configuration""" + changed = changed_support_mode = changed_lags = False + results = dict(changed=changed) + results['dvswitch'] = self.switch_name + changed_list = [] + + spec = vim.dvs.VmwareDistributedVirtualSwitch.ConfigSpec() + spec.configVersion = self.dvs.config.configVersion + + # Check support mode + results['support_mode'] = self.support_mode + lacp_support_mode = self.get_lacp_support_mode(self.support_mode) + if self.dvs.config.lacpApiVersion != lacp_support_mode: + changed = changed_support_mode = True + changed_list.append("support mode") + results['support_mode_previous'] = self.get_lacp_support_mode(self.dvs.config.lacpApiVersion) + spec.lacpApiVersion = lacp_support_mode + + # Check LAGs + results['link_aggregation_groups'] = self.link_aggregation_groups + if self.link_aggregation_groups and not ( + len(self.link_aggregation_groups) == 1 and self.link_aggregation_groups[0] == ''): + if self.dvs.config.lacpGroupConfig: + lacp_lag_list = [] + # Check if desired LAGs are configured + for lag in self.link_aggregation_groups: + lag_name, lag_mode, lag_uplink_number, lag_load_balancing_mode = self.get_lacp_lag_options(lag) + lag_found = False + for lacp_group in self.dvs.config.lacpGroupConfig: + if lacp_group.name == lag_name: + lag_found = True + if (lag_mode != lacp_group.mode or + lag_uplink_number != lacp_group.uplinkNum or + lag_load_balancing_mode != lacp_group.loadbalanceAlgorithm): + changed = changed_lags = True + lacp_lag_list.append( + self.create_lacp_group_spec( + 'edit', + lacp_group.key, lag_name, lag_uplink_number, lag_mode, lag_load_balancing_mode + ) + ) + break + if lag_found is False: + changed = changed_lags = True + lacp_lag_list.append( + self.create_lacp_group_spec( + 'add', None, lag_name, lag_uplink_number, lag_mode, lag_load_balancing_mode + ) + ) + # Check if LAGs need to be removed + for lacp_group in self.dvs.config.lacpGroupConfig: + lag_found = False + for lag in self.link_aggregation_groups: + result = self.get_lacp_lag_options(lag) + if lacp_group.name == result[0]: + lag_found = True + break + if lag_found is False: + changed = changed_lags = True + lacp_lag_list.append( + self.create_lacp_group_spec('remove', lacp_group.key, lacp_group.name, None, None, None) + ) + else: + changed = changed_lags = True + lacp_lag_list = [] + for lag in self.link_aggregation_groups: + lag_name, lag_mode, lag_uplink_number, lag_load_balancing_mode = self.get_lacp_lag_options(lag) + lacp_lag_list.append( + self.create_lacp_group_spec( + 'add', None, lag_name, lag_uplink_number, lag_mode, lag_load_balancing_mode + ) + ) + else: + if self.dvs.config.lacpGroupConfig: + changed = changed_lags = True + lacp_lag_list = [] + for lacp_group in self.dvs.config.lacpGroupConfig: + lacp_lag_list.append( + self.create_lacp_group_spec('remove', lacp_group.key, lacp_group.name, None, None, None) + ) + if changed_lags: + changed_list.append("link aggregation groups") + current_lags_list = [] + for lacp_group in self.dvs.config.lacpGroupConfig: + temp_lag = dict() + temp_lag['name'] = lacp_group.name + temp_lag['uplink_number'] = lacp_group.uplinkNum + temp_lag['mode'] = lacp_group.mode + temp_lag['load_balancing_mode'] = lacp_group.loadbalanceAlgorithm + current_lags_list.append(temp_lag) + results['link_aggregation_groups_previous'] = current_lags_list + + if changed: + if self.module.check_mode: + changed_suffix = ' would be changed' + else: + changed_suffix = ' changed' + if len(changed_list) > 2: + message = ', '.join(changed_list[:-1]) + ', and ' + str(changed_list[-1]) + elif len(changed_list) == 2: + message = ' and '.join(changed_list) + elif len(changed_list) == 1: + message = changed_list[0] + message += changed_suffix + if not self.module.check_mode: + if changed_support_mode and self.support_mode == 'basic' and changed_lags: + self.update_lacp_group_config(self.dvs, lacp_lag_list) + # NOTE: You need to run the task again to change the support mode to 'basic' as well + # No matter how long you sleep, you will always get the following error in vCenter: + # 'Cannot complete operation due to concurrent modification by another operation.' + # self.update_dvs_config(self.dvs, spec) + else: + if changed_support_mode: + self.update_dvs_config(self.dvs, spec) + if changed_lags: + self.update_lacp_group_config(self.dvs, lacp_lag_list) + else: + message = "LACP already configured properly" + results['changed'] = changed + results['result'] = message + + self.module.exit_json(**results) + + @staticmethod + def get_lacp_support_mode(mode): + """Get LACP support mode""" + return_mode = None + if mode == 'basic': + return_mode = 'singleLag' + elif mode == 'enhanced': + return_mode = 'multipleLag' + elif mode == 'singleLag': + return_mode = 'basic' + elif mode == 'multipleLag': + return_mode = 'enhanced' + return return_mode + + def get_lacp_lag_options(self, lag): + """Get and check LACP LAG options""" + lag_name = lag.get('name', None) + if lag_name is None: + self.module.fail_json(msg="Please specify name in lag options as it's a required parameter") + lag_mode = lag.get('mode', None) + if lag_mode is None: + self.module.fail_json(msg="Please specify mode in lag options as it's a required parameter") + lag_uplink_number = lag.get('uplink_number', None) + if lag_uplink_number is None: + self.module.fail_json(msg="Please specify uplink_number in lag options as it's a required parameter") + elif lag_uplink_number > 30: + self.module.fail_json(msg="More than 30 uplinks are not supported in a single LAG!") + lag_load_balancing_mode = lag.get('load_balancing_mode', None) + supported_lb_modes = ['srcTcpUdpPort', 'srcDestIpTcpUdpPortVlan', 'srcIpVlan', 'srcDestTcpUdpPort', + 'srcMac', 'destIp', 'destMac', 'vlan', 'srcDestIp', 'srcIpTcpUdpPortVlan', + 'srcDestIpTcpUdpPort', 'srcDestMac', 'destIpTcpUdpPort', 'srcPortId', 'srcIp', + 'srcIpTcpUdpPort', 'destIpTcpUdpPortVlan', 'destTcpUdpPort', 'destIpVlan', 'srcDestIpVlan'] + if lag_load_balancing_mode is None: + self.module.fail_json(msg="Please specify load_balancing_mode in lag options as it's a required parameter") + elif lag_load_balancing_mode not in supported_lb_modes: + self.module.fail_json(msg="The specified load balancing mode '%s' isn't supported!" % lag_load_balancing_mode) + return lag_name, lag_mode, lag_uplink_number, lag_load_balancing_mode + + @staticmethod + def create_lacp_group_spec(operation, key, name, uplink_number, mode, load_balancing_mode): + """ + Create LACP group spec + operation: add, edit, or remove + Returns: LACP group spec + """ + lacp_spec = vim.dvs.VmwareDistributedVirtualSwitch.LacpGroupSpec() + lacp_spec.operation = operation + lacp_spec.lacpGroupConfig = vim.dvs.VmwareDistributedVirtualSwitch.LacpGroupConfig() + lacp_spec.lacpGroupConfig.name = name + if operation in ('edit', 'remove'): + lacp_spec.lacpGroupConfig.key = key + if not operation == 'remove': + lacp_spec.lacpGroupConfig.uplinkNum = uplink_number + lacp_spec.lacpGroupConfig.mode = mode + lacp_spec.lacpGroupConfig.loadbalanceAlgorithm = load_balancing_mode + # greyed out in vSphere Client!? + # lacp_spec.vlan = vim.dvs.VmwareDistributedVirtualSwitch.LagVlanConfig() + # lacp_spec.vlan.vlanId = [vim.NumericRange(...)] + # lacp_spec.ipfix = vim.dvs.VmwareDistributedVirtualSwitch.LagIpfixConfig() + # lacp_spec.ipfix.ipfixEnabled = True/False + return lacp_spec + + def update_dvs_config(self, switch_object, spec): + """Update DVS config""" + try: + task = switch_object.ReconfigureDvs_Task(spec) + result = wait_for_task(task) + except TaskError as invalid_argument: + self.module.fail_json( + msg="Failed to update DVS : %s" % to_native(invalid_argument) + ) + return result + + def update_lacp_group_config(self, switch_object, lacp_group_spec): + """Update LACP group config""" + try: + task = switch_object.UpdateDVSLacpGroupConfig_Task(lacpGroupSpec=lacp_group_spec) + result = wait_for_task(task) + except vim.fault.DvsFault as dvs_fault: + self.module.fail_json(msg="Update failed due to DVS fault : %s" % to_native(dvs_fault)) + except vmodl.fault.NotSupported as not_supported: + self.module.fail_json( + msg="Multiple Link Aggregation Control Protocol groups not supported on the switch : %s" % + to_native(not_supported) + ) + except TaskError as invalid_argument: + self.module.fail_json( + msg="Failed to update Link Aggregation Group : %s" % to_native(invalid_argument) + ) + return result + + +def main(): + """Main""" + argument_spec = vmware_argument_spec() + argument_spec.update( + dict( + switch=dict(required=True, aliases=['dvswitch']), + support_mode=dict(default='basic', choices=['basic', 'enhanced']), + link_aggregation_groups=dict(default=[], type='list'), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + vmware_dvswitch_lacp = VMwareDvSwitchLacp(module) + vmware_dvswitch_lacp.ensure() + + +if __name__ == '__main__': + main()