diff --git a/lib/ansible/module_utils/common/network.py b/lib/ansible/module_utils/common/network.py index f385c2525bb..cf79db511e7 100644 --- a/lib/ansible/module_utils/common/network.py +++ b/lib/ansible/module_utils/common/network.py @@ -3,6 +3,7 @@ # General networking tools that may be used by all modules +import re from struct import pack from socket import inet_ntoa @@ -143,3 +144,15 @@ def to_bits(val): for octet in val.split('.'): bits += bin(int(octet))[2:].zfill(8) return str + + +def is_mac(mac_address): + """ + Validate MAC address for given string + Args: + mac_address: string to validate as MAC address + + Returns: (Boolean) True if string is valid MAC address, otherwise False + """ + mac_addr_regex = re.compile('[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$') + return bool(mac_addr_regex.match(mac_address.lower())) diff --git a/lib/ansible/module_utils/vmware.py b/lib/ansible/module_utils/vmware.py index f7e63e30493..0134cb1162e 100644 --- a/lib/ansible/module_utils/vmware.py +++ b/lib/ansible/module_utils/vmware.py @@ -832,7 +832,7 @@ class PyVmomi(object): def get_managed_objects_properties(self, vim_type, properties=None): """ - Function to look up a Managed Object Reference in vCenter / ESXi Environment + Look up a Managed Object Reference in vCenter / ESXi Environment :param vim_type: Type of vim object e.g, for datacenter - vim.Datacenter :param properties: List of properties related to vim object e.g. Name :return: local content object @@ -880,7 +880,7 @@ class PyVmomi(object): # Virtual Machine related functions def get_vm(self): """ - Function to find unique virtual machine either by UUID or Name. + Find unique virtual machine either by UUID or Name. Returns: virtual machine object if found, else None. """ @@ -967,7 +967,7 @@ class PyVmomi(object): def gather_facts(self, vm): """ - Function to gather facts of virtual machine. + Gather facts of virtual machine. Args: vm: Name of virtual machine. @@ -979,7 +979,7 @@ class PyVmomi(object): @staticmethod def get_vm_path(content, vm_name): """ - Function to find the path of virtual machine. + Find the path of virtual machine. Args: content: VMware content object vm_name: virtual machine managed object @@ -1088,7 +1088,7 @@ class PyVmomi(object): def get_all_host_objs(self, cluster_name=None, esxi_host_name=None): """ - Function to get all host system managed object + Get all host system managed object Args: cluster_name: Name of Cluster @@ -1163,7 +1163,7 @@ class PyVmomi(object): def get_all_port_groups_by_host(self, host_system): """ - Function to get all Port Group by host + Get all Port Group by host Args: host_system: Name of Host System @@ -1174,10 +1174,48 @@ class PyVmomi(object): pgs_list.append(pg) return pgs_list + def find_network_by_name(self, network_name=None): + """ + Get network specified by name + Args: + network_name: Name of network + + Returns: List of network managed objects + """ + networks = [] + + if not network_name: + return networks + + objects = self.get_managed_objects_properties(vim_type=vim.Network, properties=['name']) + + for temp_vm_object in objects: + if len(temp_vm_object.propSet) != 1: + continue + for temp_vm_object_property in temp_vm_object.propSet: + if temp_vm_object_property.val == self.params['name']: + networks.append(temp_vm_object.obj) + break + return networks + + def network_exists_by_name(self, network_name=None): + """ + Check if network with a specified name exists or not + Args: + network_name: Name of network + + Returns: True if network exists else False + """ + ret = False + if not network_name: + return ret + ret = True if self.find_network_by_name(network_name=network_name) else False + return ret + # Datacenter def find_datacenter_by_name(self, datacenter_name): """ - Function to get datacenter managed object by name + Get datacenter managed object by name Args: datacenter_name: Name of datacenter @@ -1189,7 +1227,7 @@ class PyVmomi(object): def find_datastore_by_name(self, datastore_name): """ - Function to get datastore managed object by name + Get datastore managed object by name Args: datastore_name: Name of datastore @@ -1201,7 +1239,7 @@ class PyVmomi(object): # Datastore cluster def find_datastore_cluster_by_name(self, datastore_cluster_name): """ - Function to get datastore cluster managed object by name + Get datastore cluster managed object by name Args: datastore_cluster_name: Name of datastore cluster diff --git a/lib/ansible/module_utils/xenserver.py b/lib/ansible/module_utils/xenserver.py index ee2c9c982a0..570677775e0 100644 --- a/lib/ansible/module_utils/xenserver.py +++ b/lib/ansible/module_utils/xenserver.py @@ -18,6 +18,7 @@ except ImportError: pass from ansible.module_utils.basic import env_fallback +from ansible.module_utils.common.network import is_mac from ansible.module_utils.ansible_release import __version__ as ANSIBLE_VERSION @@ -72,19 +73,6 @@ def module_to_xapi_vm_power_state(power_state): return vm_power_state_map.get(power_state) -def is_valid_mac_addr(mac_addr): - """Validates given string as MAC address. - - Args: - mac_addr (str): string to validate as MAC address. - - Returns: - bool: True if string is valid MAC address, else False. - """ - mac_addr_regex = re.compile('[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$') - return bool(mac_addr_regex.match(mac_addr.lower())) - - def is_valid_ip_addr(ip_addr): """Validates given string as IPv4 address for given string. diff --git a/lib/ansible/modules/cloud/smartos/nictagadm.py b/lib/ansible/modules/cloud/smartos/nictagadm.py index 1a482bddc36..2c5c1199e22 100644 --- a/lib/ansible/modules/cloud/smartos/nictagadm.py +++ b/lib/ansible/modules/cloud/smartos/nictagadm.py @@ -103,7 +103,7 @@ state: ''' from ansible.module_utils.basic import AnsibleModule -import re +from ansible.module_utils.common.network import is_mac class NicTag(object): @@ -121,9 +121,7 @@ class NicTag(object): self.nictagadm_bin = self.module.get_bin_path('nictagadm', True) def is_valid_mac(self): - if re.match("[0-9a-f]{2}([:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", self.mac.lower()): - return True - return False + return is_mac(self.mac.lower()) def nictag_exists(self): cmd = [self.nictagadm_bin] diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest.py b/lib/ansible/modules/cloud/vmware/vmware_guest.py index 0234e9b46a4..81e238affa1 100644 --- a/lib/ansible/modules/cloud/vmware/vmware_guest.py +++ b/lib/ansible/modules/cloud/vmware/vmware_guest.py @@ -589,6 +589,7 @@ except ImportError: from random import randint from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.network import is_mac from ansible.module_utils._text import to_text, to_native from ansible.module_utils.vmware import (find_obj, gather_vm_facts, get_all_objs, compile_folder_path_for_object, serialize_spec, @@ -729,7 +730,7 @@ class PyVmomiDeviceHelper(object): nic.device.connectable.startConnected = bool(device_infos.get('start_connected', True)) nic.device.connectable.allowGuestControl = bool(device_infos.get('allow_guest_control', True)) nic.device.connectable.connected = True - if 'mac' in device_infos and self.is_valid_mac_addr(device_infos['mac']): + if 'mac' in device_infos and is_mac(device_infos['mac']): nic.device.addressType = 'manual' nic.device.macAddress = device_infos['mac'] else: @@ -737,18 +738,6 @@ class PyVmomiDeviceHelper(object): return nic - @staticmethod - def is_valid_mac_addr(mac_addr): - """ - Function to validate MAC address for given string - Args: - mac_addr: string to validate as MAC address - - Returns: (Boolean) True if string is valid MAC address, otherwise False - """ - mac_addr_regex = re.compile('[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$') - return bool(mac_addr_regex.match(mac_addr)) - def integer_value(self, input_value, name): """ Function to return int value for given input, else return error @@ -1273,7 +1262,7 @@ class PyVmomiHelper(PyVmomi): " type from ['%s']." % (network['device_type'], "', '".join(validate_device_types))) - if 'mac' in network and not PyVmomiDeviceHelper.is_valid_mac_addr(network['mac']): + if 'mac' in network and not is_mac(network['mac']): self.module.fail_json(msg="Device MAC address '%s' is invalid." " Please provide correct MAC address." % network['mac']) diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest_network.py b/lib/ansible/modules/cloud/vmware/vmware_guest_network.py new file mode 100644 index 00000000000..ad5982b2c29 --- /dev/null +++ b/lib/ansible/modules/cloud/vmware/vmware_guest_network.py @@ -0,0 +1,461 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Ansible Project +# Copyright: (c) 2019, Diane Wang +# 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_guest_network +short_description: Manage network adapters of specified virtual machine in given vCenter infrastructure +description: + - This module is used to add, reconfigure, remove network adapter of given virtual machine. + - All parameters and VMware object names are case sensitive. +version_added: '2.9' +author: + - Diane Wang (@Tomorrow9) +notes: + - Tested on vSphere 6.0, 6.5 and 6.7 +requirements: + - "python >= 2.6" + - PyVmomi +options: + name: + description: + - Name of the virtual machine. + - This is a required parameter, if parameter C(uuid) is not supplied. + type: str + uuid: + description: + - UUID of the instance to gather facts if known, this is VMware's unique identifier. + - This is a required parameter, if parameter C(name) is not supplied. + type: str + folder: + description: + - Destination folder, absolute or relative path to find an existing guest. + - This is a required parameter, only if multiple VMs are found with same name. + - The folder should include the datacenter. ESXi server's datacenter is ha-datacenter. + - 'Examples:' + - ' folder: /ha-datacenter/vm' + - ' folder: ha-datacenter/vm' + - ' folder: /datacenter1/vm' + - ' folder: datacenter1/vm' + - ' folder: /datacenter1/vm/folder1' + - ' folder: datacenter1/vm/folder1' + - ' folder: /folder1/datacenter1/vm' + - ' folder: folder1/datacenter1/vm' + - ' folder: /folder1/datacenter1/vm/folder2' + type: str + cluster: + description: + - The name of cluster where the virtual machine will run. + - This is a required parameter, if C(esxi_hostname) is not set. + - C(esxi_hostname) and C(cluster) are mutually exclusive parameters. + type: str + esxi_hostname: + description: + - The ESXi hostname where the virtual machine will run. + - This is a required parameter, if C(cluster) is not set. + - C(esxi_hostname) and C(cluster) are mutually exclusive parameters. + type: str + datacenter: + default: ha-datacenter + description: + - The datacenter name to which virtual machine belongs to. + type: str + gather_network_facts: + description: + - If set to C(True), return settings of all network adapters, other parameters are ignored. + - If set to C(False), will add, reconfigure or remove network adapters according to the parameters in C(networks). + type: bool + default: False + networks: + type: list + description: + - A list of network adapters. + - C(mac) or C(label) or C(device_type) is required to reconfigure or remove an existing network adapter. + - 'If there are multiple network adapters with the same C(device_type), you should set C(label) or C(mac) to match + one of them, or will apply changes on all network adapters with the C(device_type) specified.' + - 'C(mac), C(label), C(device_type) is the order of precedence from greatest to least if all set.' + - 'Valid attributes are:' + - ' - C(mac) (string): MAC address of the existing network adapter to be reconfigured or removed.' + - ' - C(label) (string): Label of the existing network adapter to be reconfigured or removed, e.g., "Network adapter 1".' + - ' - C(device_type) (string): Valid virtual network device types are: + C(e1000), C(e1000e), C(pcnet32), C(vmxnet2), C(vmxnet3) (default), C(sriov). + Used to add new network adapter, reconfigure or remove the existing network adapter with this type. + If C(mac) and C(label) not specified or not find network adapter by C(mac) or C(label) will use this parameter.' + - ' - C(name) (string): Name of the portgroup or distributed virtual portgroup for this interface. + When specifying distributed virtual portgroup make sure given C(esxi_hostname) or C(cluster) is associated with it.' + - ' - C(vlan) (integer): VLAN number for this interface.' + - ' - C(dvswitch_name) (string): Name of the distributed vSwitch. + This value is required if multiple distributed portgroups exists with the same name.' + - ' - C(state) (string): State of the network adapter.' + - ' If set to C(present), then will do reconfiguration for the specified network adapter.' + - ' If set to C(new), then will add the specified network adapter.' + - ' If set to C(absent), then will remove this network adapter.' + - ' - C(manual_mac) (string): Manual specified MAC address of the network adapter when creating, or reconfiguring. + If not specified when creating new network adapter, mac address will be generated automatically. + When reconfigure MAC address, VM should be in powered off state.' + - ' - C(connected) (bool): Indicates that virtual network adapter connects to the associated virtual machine.' + - ' - C(start_connected) (bool): Indicates that virtual network adapter starts with associated virtual machine powers on.' +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' +- name: Change network adapter settings of virtual machine + vmware_guest_network: + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ datacenter_name }}" + validate_certs: no + name: test-vm + gather_network_facts: false + networks: + - name: "VM Network" + state: new + manual_mac: "00:50:56:11:22:33" + - state: present + device_type: e1000e + manual_mac: "00:50:56:44:55:66" + - state: present + label: "Network adapter 3" + connected: false + - state: absent + mac: "00:50:56:44:55:77" + delegate_to: localhost + register: network_facts +''' + +RETURN = """ +network_data: + description: metadata about the virtual machine's network adapter after managing them + returned: always + type: dict + sample: { + "0": { + "label": "Network Adapter 1", + "name": "VM Network", + "device_type": "E1000E", + "mac_addr": "00:50:56:89:dc:05", + "unit_number": 7, + "wake_onlan": false, + "allow_guest_ctl": true, + "connected": true, + "start_connected": true, + }, + } +""" + +import re + +try: + from pyVmomi import vim +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.network import is_mac +from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.vmware import PyVmomi, vmware_argument_spec, wait_for_task, find_obj, get_all_objs, get_parent_datacenter + + +class PyVmomiHelper(PyVmomi): + def __init__(self, module): + super(PyVmomiHelper, self).__init__(module) + self.change_detected = False + self.config_spec = vim.vm.ConfigSpec() + self.config_spec.deviceChange = [] + self.nic_device_type = dict( + pcnet32=vim.vm.device.VirtualPCNet32, + vmxnet2=vim.vm.device.VirtualVmxnet2, + vmxnet3=vim.vm.device.VirtualVmxnet3, + e1000=vim.vm.device.VirtualE1000, + e1000e=vim.vm.device.VirtualE1000e, + sriov=vim.vm.device.VirtualSriovEthernetCard, + ) + + def get_device_type(self, device_type=None): + """ Get network adapter device type """ + if device_type and device_type in list(self.nic_device_type.keys()): + return self.nic_device_type[device_type]() + else: + self.module.fail_json(msg='Invalid network device_type %s' % device_type) + + def get_network_device(self, vm=None, mac=None, device_type=None, device_label=None): + """ + Get network adapter + """ + nic_devices = [] + nic_device = None + if vm is None: + if device_type: + return nic_devices + else: + return nic_device + + for device in vm.config.hardware.device: + if mac: + if isinstance(device, vim.vm.device.VirtualEthernetCard): + if device.macAddress == mac: + nic_device = device + break + elif device_type: + if isinstance(device, self.nic_device_type[device_type]): + nic_devices.append(device) + elif device_label: + if isinstance(device, vim.vm.device.VirtualEthernetCard): + if device.deviceInfo.label == device_label: + nic_device = device + break + if device_type: + return nic_devices + else: + return nic_device + + def get_network_device_by_mac(self, vm=None, mac=None): + """ Get network adapter with the specified mac address""" + return self.get_network_device(vm=vm, mac=mac) + + def get_network_devices_by_type(self, vm=None, device_type=None): + """ Get network adapter list with the name type """ + return self.get_network_device(vm=vm, device_type=device_type) + + def get_network_device_by_label(self, vm=None, device_label=None): + """ Get network adapter with the specified label """ + return self.get_network_device(vm=vm, device_label=device_label) + + def create_network_adapter(self, device_info): + nic = vim.vm.device.VirtualDeviceSpec() + nic.device = self.get_device_type(device_type=device_info.get('device_type', 'vmxnet3')) + nic.device.deviceInfo = vim.Description() + nic.device.deviceInfo.summary = device_info['name'] + nic.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo() + nic.device.backing.deviceName = device_info['name'] + nic.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo() + nic.device.connectable.startConnected = device_info.get('start_connected', True) + nic.device.connectable.allowGuestControl = True + nic.device.connectable.connected = device_info.get('connected', True) + if 'manual_mac' in device_info: + nic.device.addressType = 'manual' + nic.device.macAddress = device_info['manual_mac'] + else: + nic.device.addressType = 'generated' + + return nic + + def get_network_facts(self, vm_obj): + network_facts = dict() + if vm_obj is None: + return network_facts + + nic_index = 0 + for nic in vm_obj.config.hardware.device: + nic_type = None + if isinstance(nic, vim.vm.device.VirtualPCNet32): + nic_type = 'PCNet32' + elif isinstance(nic, vim.vm.device.VirtualVmxnet2): + nic_type = 'VMXNET2' + elif isinstance(nic, vim.vm.device.VirtualVmxnet3): + nic_type = 'VMXNET3' + elif isinstance(nic, vim.vm.device.VirtualE1000): + nic_type = 'E1000' + elif isinstance(nic, vim.vm.device.VirtualE1000e): + nic_type = 'E1000E' + elif isinstance(nic, vim.vm.device.VirtualSriovEthernetCard): + nic_type = 'SriovEthernetCard' + if nic_type is not None: + network_facts[nic_index] = dict( + device_type=nic_type, + label=nic.deviceInfo.label, + name=nic.deviceInfo.summary, + mac_addr=nic.macAddress, + unit_number=nic.unitNumber, + wake_onlan=nic.wakeOnLanEnabled, + allow_guest_ctl=nic.connectable.allowGuestControl, + connected=nic.connectable.connected, + start_connected=nic.connectable.startConnected, + ) + nic_index += 1 + + return network_facts + + def sanitize_network_params(self): + network_list = [] + valid_state = ['new', 'present', 'absent'] + if len(self.params['networks']) != 0: + for network in self.params['networks']: + if 'state' not in network or network['state'].lower() not in valid_state: + self.module.fail_json(msg="Network adapter state not specified or invalid: '%s', valid values: " + "%s" % (network.get('state', ''), valid_state)) + # add new network adapter but no name specified + if network['state'].lower() == 'new' and 'name' not in network and 'vlan' not in network: + self.module.fail_json(msg="Please specify at least network name or VLAN name for adding new network adapter.") + if network['state'].lower() == 'new' and 'mac' in network: + self.module.fail_json(msg="networks.mac is used for vNIC reconfigure, but networks.state is set to 'new'.") + if network['state'].lower() == 'present' and 'mac' not in network and 'label' not in network and 'device_type' not in network: + self.module.fail_json(msg="Should specify 'mac', 'label' or 'device_type' parameter to reconfigure network adapter") + if 'connected' in network: + if not isinstance(network['connected'], bool): + self.module.fail_json(msg="networks.connected parameter should be boolean.") + if network['state'].lower() == 'new' and not network['connected']: + network['start_connected'] = False + if 'start_connected' in network: + if not isinstance(network['start_connected'], bool): + self.module.fail_json(msg="networks.start_connected parameter should be boolean.") + if network['state'].lower() == 'new' and not network['start_connected']: + network['connected'] = False + # specified network not exist + if 'name' in network and not self.network_exists_by_name(self.content, network['name']): + self.module.fail_json(msg="Network '%(name)s' does not exist." % network) + elif 'vlan' in network: + objects = get_all_objs(self.content, [vim.dvs.DistributedVirtualPortgroup]) + dvps = [x for x in objects if to_text(get_parent_datacenter(x).name) == to_text(self.params['datacenter'])] + for dvp in dvps: + if hasattr(dvp.config.defaultPortConfig, 'vlan') and \ + isinstance(dvp.config.defaultPortConfig.vlan.vlanId, int) and \ + str(dvp.config.defaultPortConfig.vlan.vlanId) == str(network['vlan']): + network['name'] = dvp.config.name + break + if 'dvswitch_name' in network and \ + dvp.config.distributedVirtualSwitch.name == network['dvswitch_name'] and \ + dvp.config.name == network['vlan']: + network['name'] = dvp.config.name + break + if dvp.config.name == network['vlan']: + network['name'] = dvp.config.name + break + else: + self.module.fail_json(msg="VLAN '%(vlan)s' does not exist." % network) + + if 'device_type' in network and network['device_type'] not in list(self.nic_device_type.keys()): + self.module.fail_json(msg="Device type specified '%s' is invalid. " + "Valid types %s " % (network['device_type'], list(self.nic_device_type.keys()))) + + if ('mac' in network and not is_mac(network['mac'])) or \ + ('manual_mac' in network and not is_mac(network['manual_mac'])): + self.module.fail_json(msg="Device MAC address '%s' or manual set MAC address %s is invalid. " + "Please provide correct MAC address." % (network['mac'], network['manual_mac'])) + + network_list.append(network) + + return network_list + + def get_network_config_spec(self, vm_obj, network_list): + # create network adapter config spec for adding, editing, removing + for network in network_list: + # add new network adapter + if network['state'].lower() == 'new': + nic_spec = self.create_network_adapter(network) + nic_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add + self.change_detected = True + self.config_spec.deviceChange.append(nic_spec) + # reconfigure network adapter or remove network adapter + else: + nic_devices = [] + if 'mac' in network: + nic = self.get_network_device_by_mac(vm_obj, mac=network['mac']) + if nic is not None: + nic_devices.append(nic) + if 'label' in network and len(nic_devices) == 0: + nic = self.get_network_device_by_label(vm_obj, device_label=network['label']) + if nic is not None: + nic_devices.append(nic) + if 'device_type' in network and len(nic_devices) == 0: + nic_devices = self.get_network_devices_by_type(vm_obj, device_type=network['device_type']) + if len(nic_devices) != 0: + for nic_device in nic_devices: + nic_spec = vim.vm.device.VirtualDeviceSpec() + if network['state'].lower() == 'present': + nic_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit + nic_spec.device = nic_device + if 'start_connected' in network and nic_device.connectable.startConnected != network['start_connected']: + nic_device.connectable.startConnected = network['start_connected'] + self.change_detected = True + if 'connected' in network and nic_device.connectable.connected != network['connected']: + nic_device.connectable.connected = network['connected'] + self.change_detected = True + if 'name' in network and nic_device.deviceInfo.summary != network['name']: + nic_device.deviceInfo.summary = network['name'] + self.change_detected = True + if 'manual_mac' in network and nic_device.macAddress != network['manual_mac']: + if vm_obj.runtime.powerState != vim.VirtualMachinePowerState.poweredOff: + self.module.fail_json(msg='Expected power state is poweredOff to reconfigure MAC address') + nic_device.addressType = 'manual' + nic_device.macAddress = network['manual_mac'] + self.change_detected = True + if self.change_detected: + self.config_spec.deviceChange.append(nic_spec) + elif network['state'].lower() == 'absent': + nic_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.remove + nic_spec.device = nic_device + self.change_detected = True + self.config_spec.deviceChange.append(nic_spec) + else: + self.module.fail_json(msg='Unable to find the specified network adapter: %s' % network) + + def reconfigure_vm_network(self, vm_obj): + network_list = self.sanitize_network_params() + # gather network adapter facts only + if (self.params['gather_network_facts'] is not None and self.params['gather_network_facts']) or len(network_list) == 0: + results = {'changed': False, 'failed': False, 'network_data': self.get_network_facts(vm_obj)} + # do reconfigure then gather facts + else: + self.get_network_config_spec(vm_obj, network_list) + try: + task = vm_obj.ReconfigVM_Task(spec=self.config_spec) + wait_for_task(task) + except vim.fault.InvalidDeviceSpec as e: + self.module.fail_json(msg="Failed to configure network adapter on given virtual machine due to invalid" + " device spec : %s" % to_native(e.msg), + details="Please check ESXi server logs for more details.") + except vim.fault.RestrictedVersion as e: + self.module.fail_json(msg="Failed to reconfigure virtual machine due to" + " product versioning restrictions: %s" % to_native(e.msg)) + if task.info.state == 'error': + results = {'changed': self.change_detected, 'failed': True, 'msg': task.info.error.msg} + else: + network_facts = self.get_network_facts(vm_obj) + results = {'changed': self.change_detected, 'failed': False, 'network_data': network_facts} + + return results + + +def main(): + argument_spec = vmware_argument_spec() + argument_spec.update( + name=dict(type='str'), + uuid=dict(type='str'), + folder=dict(type='str'), + datacenter=dict(type='str', default='ha-datacenter'), + esxi_hostname=dict(type='str'), + cluster=dict(type='str'), + gather_network_facts=dict(type='bool', default=False), + networks=dict(type='list', default=[]) + ) + + module = AnsibleModule(argument_spec=argument_spec, required_one_of=[['name', 'uuid']]) + pyv = PyVmomiHelper(module) + vm = pyv.get_vm() + if not vm: + module.fail_json(msg='Unable to find the specified virtual machine uuid: %s, name: %s ' + % ((module.params.get('uuid')), (module.params.get('name')))) + + result = pyv.reconfigure_vm_network(vm) + if result['failed']: + module.fail_json(**result) + else: + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/xenserver/xenserver_guest.py b/lib/ansible/modules/cloud/xenserver/xenserver_guest.py index 3ac13beade9..4f3be77d73d 100644 --- a/lib/ansible/modules/cloud/xenserver/xenserver_guest.py +++ b/lib/ansible/modules/cloud/xenserver/xenserver_guest.py @@ -438,10 +438,11 @@ except ImportError: pass from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.network import is_mac from ansible.module_utils import six from ansible.module_utils.xenserver import (xenserver_common_argument_spec, XAPI, XenServerObject, get_object_ref, gather_vm_params, gather_vm_facts, set_vm_power_state, wait_for_vm_ip_address, - is_valid_mac_addr, is_valid_ip_addr, is_valid_ip_netmask, is_valid_ip_prefix, + is_valid_ip_addr, is_valid_ip_netmask, is_valid_ip_prefix, ip_prefix_to_netmask, ip_netmask_to_prefix, is_valid_ip6_addr, is_valid_ip6_prefix) @@ -1406,7 +1407,7 @@ class XenServerVM(XenServerObject): if network_mac is not None: network_mac = network_mac.lower() - if not is_valid_mac_addr(network_mac): + if not is_mac(network_mac): self.module.fail_json(msg="VM check networks[%s]: specified MAC address '%s' is not valid!" % (position, network_mac)) # IPv4 reconfiguration. diff --git a/test/integration/targets/vmware_guest_network/aliases b/test/integration/targets/vmware_guest_network/aliases new file mode 100644 index 00000000000..3eede2cbf01 --- /dev/null +++ b/test/integration/targets/vmware_guest_network/aliases @@ -0,0 +1,3 @@ +cloud/vcenter +shippable/vcenter/group1 +needs/target/prepare_vmware_tests diff --git a/test/integration/targets/vmware_guest_network/tasks/main.yml b/test/integration/targets/vmware_guest_network/tasks/main.yml new file mode 100644 index 00000000000..a9c059c1437 --- /dev/null +++ b/test/integration/targets/vmware_guest_network/tasks/main.yml @@ -0,0 +1,87 @@ +# Test code for the vmware_guest_network module +# Copyright: (c) 2019, Diane Wang (Tomorrow9) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- when: vcsim is not defined + block: + - import_role: + name: prepare_vmware_tests + vars: + setup_attach_host: true + setup_datastore: true + setup_virtualmachines: true + + - name: gather network adapters' facts of the virtual machine + vmware_guest_network: + validate_certs: False + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + name: "{{ infra.vm_list[0] }}" + gather_network_facts: true + register: netadapter_facts + - debug: var=netadapter_facts + - name: get number of existing netowrk adapters + set_fact: + netadapter_num: "{{ netadapter_facts.network_data | length }}" + + - name: add new network adapters to virtual machine + vmware_guest_network: + validate_certs: False + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + name: "{{ infra.vm_list[0] }}" + networks: + - name: "VM Network" + state: new + device_type: e1000e + manual_mac: "00:50:56:58:59:60" + - name: "VM Network" + state: new + device_type: vmxnet3 + manual_mac: "00:50:56:58:59:61" + register: add_netadapter + - debug: var=add_netadapter + - name: assert the new netowrk adapters were added to VM + assert: + that: + - "add_netadapter.changed == true" + - "{{ add_netadapter.network_data | length | int }} == {{ netadapter_num | int + 2 }}" + + - name: delete one specified network adapter + vmware_guest_network: + validate_certs: False + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + name: "{{ infra.vm_list[0] }}" + networks: + - state: absent + mac: "00:50:56:58:59:60" + register: del_netadapter + - debug: var=del_netadapter + - name: assert the network adapter was removed + assert: + that: + - "del_netadapter.changed == true" + - "{{ del_netadapter.network_data | length | int }} == {{ netadapter_num | int + 1 }}" + + - name: disconnect one specified network adapter + vmware_guest_network: + validate_certs: False + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + name: "{{ infra.vm_list[0] }}" + networks: + - state: present + mac: "00:50:56:58:59:61" + connected: false + register: disc_netadapter + - debug: var=disc_netadapter + - name: assert the network adapter was disconnected + assert: + that: + - "disc_netadapter.changed == true" + - "{{ disc_netadapter.network_data[netadapter_num]['connected'] }} == false" diff --git a/test/units/module_utils/xenserver/test_netaddr_functions.py b/test/units/module_utils/xenserver/test_netaddr_functions.py index 8218ea68de8..43a7b4be308 100644 --- a/test/units/module_utils/xenserver/test_netaddr_functions.py +++ b/test/units/module_utils/xenserver/test_netaddr_functions.py @@ -10,7 +10,7 @@ __metaclass__ = type import pytest from .FakeAnsibleModule import FakeAnsibleModule, ExitJsonException, FailJsonException - +from ansible.module_utils.common.network import is_mac testcase_is_valid_mac_addr = [ ('A4-23-8D-F8-C9-E5', True), @@ -138,7 +138,7 @@ testcase_is_valid_ip6_prefix = [ @pytest.mark.parametrize('mac_addr, result', testcase_is_valid_mac_addr) def test_is_valid_mac_addr(xenserver, mac_addr, result): """Tests against examples of valid and invalid mac addresses.""" - assert xenserver.is_valid_mac_addr(mac_addr) is result + assert is_mac(mac_addr) is result @pytest.mark.parametrize('ip_addr, result', testcase_is_valid_ip_addr)