1#!/usr/local/bin/python3.8
2#  Copyright: (c) 2020, Ansible Project
3#  Copyright: (c) 2019, Diane Wang <dianew@vmware.com>
4#  GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5from __future__ import absolute_import, division, print_function
6__metaclass__ = type
7
8
9DOCUMENTATION = r'''
10---
11module: vmware_guest_network
12short_description: Manage network adapters of specified virtual machine in given vCenter infrastructure
13description:
14  - This module is used to add, reconfigure, remove network adapter of given virtual machine.
15version_added: '1.0.0'
16requirements:
17  - "python >= 2.7"
18  - "PyVmomi"
19author:
20  - Diane Wang (@Tomorrow9) <dianew@vmware.com>
21notes:
22  - Tested on vSphere 6.0, 6.5 and 6.7
23  - For backwards compatibility network_data is returned when using the gather_network_info and networks parameters
24options:
25  name:
26    description:
27      - Name of virtual machine
28      - Required if C(uuid) or C(moid) is not supplied.
29    type: str
30  uuid:
31    description:
32      - vm uuid
33      - Required if C(name) or C(moid) is not supplied.
34    type: str
35  use_instance_uuid:
36    description:
37      - Whether to use the VMware instance UUID rather than the BIOS UUID.
38    default: False
39    type: bool
40  moid:
41    description:
42      - Managed Object ID of the instance to manage if known, this is a unique identifier only within a single vCenter instance.
43      - Required if C(uuid) or C(name) is not supplied.
44    type: str
45  folder:
46    description:
47      - Folder location of given VM, this is only required when there's multiple VM's with the same name.
48    type: str
49  datacenter:
50    default: ha-datacenter
51    description:
52      - Datacenter the VM belongs to.
53    type: str
54  cluster:
55    description:
56      - Name of cluster where VM belongs to.
57    type: str
58  esxi_hostname:
59    description:
60      - The hostname of the ESXi host where the VM belongs to.
61    type: str
62  mac_address:
63    description:
64      - MAC address of the NIC that should be altered, if a MAC address is not supplied a new nic will be created.
65      - Required when I(state=absent).
66    type: str
67  vlan_id:
68    description:
69      - VLAN id associated with the network.
70    type: int
71  network_name:
72    description:
73      - Name of network in vSphere.
74    type: str
75  device_type:
76    default: vmxnet3
77    description:
78      - Type of virtual network device.
79      - 'Valid choices are - C(e1000), C(e1000e), C(pcnet32), C(vmxnet2), C(vmxnet3) (default), C(sriov).'
80    type: str
81  label:
82    description:
83      - Alter the name of the network adapter.
84    type: str
85  switch:
86    description:
87      - Name of the (dv)switch for destination network, this is only required for dvswitches.
88    type: str
89  guest_control:
90    default: true
91    description:
92      - Enables guest control over whether the connectable device is connected.
93    type: bool
94  state:
95    default: present
96    choices: [ 'present', 'absent' ]
97    description:
98      - NIC state.
99      - When C(state=present), a nic will be added if a mac address or label does not previously exists or is unset.
100      - When C(state=absent), the I(mac_address) parameter has to be set.
101    type: str
102  start_connected:
103    default: True
104    description:
105      - If NIC should be connected to network on startup.
106    type: bool
107  wake_onlan:
108    default: False
109    description:
110      - Enable wake on LAN.
111    type: bool
112  connected:
113    default: True
114    description:
115      - If NIC should be connected to the network.
116    type: bool
117  directpath_io:
118    default: False
119    description:
120      - Enable Universal Pass-through (UPT).
121      - Only compatible with the C(vmxnet3) device type.
122    type: bool
123  force:
124    default: false
125    description:
126      - Force adapter creation even if an existing adapter is attached to the same network.
127    type: bool
128  gather_network_info:
129    aliases:
130      - gather_network_facts
131    default: False
132    description:
133      - Return information about current guest network adapters.
134    type: bool
135  networks:
136    type: list
137    elements: dict
138    description:
139      - This method will be deprecated, use loops in your playbook for multiple interfaces instead.
140      - A list of network adapters.
141      - C(mac) or C(label) or C(device_type) is required to reconfigure or remove an existing network adapter.
142      - 'If there are multiple network adapters with the same C(device_type), you should set C(label) or C(mac) to match
143         one of them, or will apply changes on all network adapters with the C(device_type) specified.'
144      - 'C(mac), C(label), C(device_type) is the order of precedence from greatest to least if all set.'
145    suboptions:
146      mac:
147        type: str
148        description:
149        - MAC address of the existing network adapter to be reconfigured or removed.
150      label:
151        type: str
152        description:
153        - Label of the existing network adapter to be reconfigured or removed, e.g., "Network adapter 1".
154      device_type:
155        type: str
156        description:
157        - 'Valid virtual network device types are C(e1000), C(e1000e), C(pcnet32), C(vmxnet2), C(vmxnet3) (default), C(sriov).'
158        - Used to add new network adapter, reconfigure or remove the existing network adapter with this type.
159        - If C(mac) and C(label) not specified or not find network adapter by C(mac) or C(label) will use this parameter.
160      name:
161        type: str
162        description:
163        - Name of the portgroup or distributed virtual portgroup for this interface.
164        - When specifying distributed virtual portgroup make sure given C(esxi_hostname) or C(cluster) is associated with it.
165      vlan:
166        type: int
167        description:
168        - VLAN number for this interface.
169      dvswitch_name:
170        type: str
171        description:
172        - Name of the distributed vSwitch.
173        - This value is required if multiple distributed portgroups exists with the same name.
174      state:
175        type: str
176        description:
177        - State of the network adapter.
178        - If set to C(present), then will do reconfiguration for the specified network adapter.
179        - If set to C(new), then will add the specified network adapter.
180        - If set to C(absent), then will remove this network adapter.
181      manual_mac:
182        type: str
183        description:
184        - Manual specified MAC address of the network adapter when creating, or reconfiguring.
185        - If not specified when creating new network adapter, mac address will be generated automatically.
186        - When reconfigure MAC address, VM should be in powered off state.
187      connected:
188        type: bool
189        description:
190        - Indicates that virtual network adapter connects to the associated virtual machine.
191      start_connected:
192        type: bool
193        description:
194        - Indicates that virtual network adapter starts with associated virtual machine powers on.
195      directpath_io:
196        type: bool
197        description:
198        - If set, Universal Pass-Through (UPT or DirectPath I/O) will be enabled on the network adapter.
199        - UPT is only compatible for Vmxnet3 adapter.
200extends_documentation_fragment:
201- community.vmware.vmware.documentation
202'''
203
204EXAMPLES = r'''
205- name: change network for 00:50:56:11:22:33 on vm01.domain.fake
206  community.vmware.vmware_guest_network:
207    hostname: "{{ vcenter_hostname }}"
208    username: "{{ vcenter_username }}"
209    password: "{{ vcenter_password }}"
210    datacenter: "{{ datacenter_name }}"
211    name: vm01.domain.fake
212    mac_address: 00:50:56:11:22:33
213    network_name: admin-network
214    state: present
215
216- name: add a nic on network with vlan id 2001 for 422d000d-2000-ffff-0000-b00000000000
217  community.vmware.vmware_guest_network:
218    hostname: "{{ vcenter_hostname }}"
219    username: "{{ vcenter_username }}"
220    password: "{{ vcenter_password }}"
221    datacenter: "{{ datacenter_name }}"
222    uuid: 422d000d-2000-ffff-0000-b00000000000
223    vlan_id: 2001
224
225- name: remove nic with mac 00:50:56:11:22:33 from vm01.domain.fake
226  community.vmware.vmware_guest_network:
227    hostname: "{{ vcenter_hostname }}"
228    username: "{{ vcenter_username }}"
229    password: "{{ vcenter_password }}"
230    datacenter: "{{ datacenter_name }}"
231    mac_address: 00:50:56:11:22:33
232    name: vm01.domain.fake
233    state: absent
234
235- name: add multiple nics to vm01.domain.fake
236  community.vmware.vmware_guest_network:
237    hostname: "{{ vcenter_hostname }}"
238    username: "{{ vcenter_username }}"
239    password: "{{ vcenter_password }}"
240    datacenter: "{{ datacenter_name }}"
241    name: vm01.domain.fake
242    state: present
243    vlan_id: "{{ item.vlan_id | default(omit) }}"
244    network_name: "{{ item.network_name | default(omit) }}"
245    connected: "{{ item.connected | default(omit) }}"
246  loop:
247    - vlan_id: 2000
248      connected: false
249    - network_name: guest-net
250      connected: true
251'''
252
253RETURN = r'''
254network_info:
255  description: metadata about the virtual machine network adapters
256  returned: always
257  type: list
258  sample:
259    "network_info": [
260        {
261            "mac_address": "00:50:56:AA:AA:AA",
262            "allow_guest_ctl": true,
263            "connected": true,
264            "device_type": "vmxnet3",
265            "label": "Network adapter 2",
266            "network_name": "admin-net",
267            "start_connected": true,
268            "switch": "vSwitch0",
269            "unit_number": 8,
270            "vlan_id": 10,
271            "wake_onlan": false
272        },
273        {
274            "mac_address": "00:50:56:BB:BB:BB",
275            "allow_guest_ctl": true,
276            "connected": true,
277            "device_type": "vmxnet3",
278            "label": "Network adapter 1",
279            "network_name": "guest-net",
280            "start_connected": true,
281            "switch": "vSwitch0",
282            "unit_number": 7,
283            "vlan_id": 10,
284            "wake_onlan": true
285        }
286    ]
287network_data:
288  description: For backwards compatibility, metadata about the virtual machine network adapters
289  returned: when using gather_network_info or networks parameters
290  type: dict
291  sample:
292    "network_data": {
293        '0': {
294            "mac_addr": "00:50:56:AA:AA:AA",
295            "mac_address": "00:50:56:AA:AA:AA",
296            "allow_guest_ctl": true,
297            "connected": true,
298            "device_type": "vmxnet3",
299            "label": "Network adapter 2",
300            "name": "admin-net",
301            "network_name": "admin-net",
302            "start_connected": true,
303            "switch": "vSwitch0",
304            "unit_number": 8,
305            "vlan_id": 10,
306            "wake_onlan": false
307        },
308        '1': {
309            "mac_addr": "00:50:56:BB:BB:BB",
310            "mac_address": "00:50:56:BB:BB:BB",
311            "allow_guest_ctl": true,
312            "connected": true,
313            "device_type": "vmxnet3",
314            "label": "Network adapter 1",
315            "name": "guest-net",
316            "network_name": "guest-net",
317            "start_connected": true,
318            "switch": "vSwitch0",
319            "unit_number": 7,
320            "vlan_id": 10,
321            "wake_onlan": true
322        }
323    }
324
325'''
326
327try:
328    from pyVmomi import vim
329except ImportError:
330    pass
331
332import copy
333from ansible.module_utils.basic import AnsibleModule
334from ansible_collections.community.vmware.plugins.module_utils.vmware import PyVmomi, vmware_argument_spec, wait_for_task
335
336
337class PyVmomiHelper(PyVmomi):
338    def __init__(self, module):
339        super(PyVmomiHelper, self).__init__(module)
340        self.change_detected = False
341        self.nic_device_type = dict(
342            pcnet32=vim.vm.device.VirtualPCNet32,
343            vmxnet2=vim.vm.device.VirtualVmxnet2,
344            vmxnet3=vim.vm.device.VirtualVmxnet3,
345            e1000=vim.vm.device.VirtualE1000,
346            e1000e=vim.vm.device.VirtualE1000e,
347            sriov=vim.vm.device.VirtualSriovEthernetCard,
348        )
349
350    def _get_network_object(self, vm_obj, network_params=None):
351        '''
352        return network object matching given parameters
353        :param vm_obj: vm object
354        :param network_params: dict containing parameters from deprecated networks list method
355        :return: network object
356        :rtype: object
357        '''
358        if not self.params['esxi_hostname'] or not self.params['cluster']:
359            compute_resource = vm_obj.runtime.host
360        else:
361            compute_resource = self._get_compute_resource_by_name()
362
363        pg_lookup = {}
364        if network_params:
365            vlan_id = network_params['vlan_id']
366            network_name = network_params['network_name']
367            switch_name = network_params['switch']
368        else:
369            vlan_id = self.params['vlan_id']
370            network_name = self.params['network_name']
371            switch_name = self.params['switch']
372
373        for pg in vm_obj.runtime.host.config.network.portgroup:
374            pg_lookup[pg.spec.name] = {'switch': pg.spec.vswitchName, 'vlan_id': pg.spec.vlanId}
375
376        if compute_resource:
377            for network in compute_resource.network:
378                if isinstance(network, vim.dvs.DistributedVirtualPortgroup):
379                    dvs = network.config.distributedVirtualSwitch
380                    if (switch_name and dvs.config.name == switch_name) or not switch_name:
381                        if network.config.name == network_name:
382                            return network
383                        if hasattr(network.config.defaultPortConfig.vlan, 'vlanId') and \
384                           network.config.defaultPortConfig.vlan.vlanId == vlan_id:
385                            return network
386                        if hasattr(network.config.defaultPortConfig.vlan, 'pvlanId') and \
387                           network.config.defaultPortConfig.vlan.pvlanId == vlan_id:
388                            return network
389                elif isinstance(network, vim.Network):
390                    if network_name and network_name == network.name:
391                        return network
392                    if vlan_id:
393                        for k in pg_lookup.keys():
394                            if vlan_id == pg_lookup[k]['vlan_id']:
395                                if k == network.name:
396                                    return network
397                                break
398        return None
399
400    def _get_vlanid_from_network(self, network):
401        '''
402        get the vlan id from network object
403        :param network: network object to expect, either vim.Network or vim.dvs.DistributedVirtualPortgroup
404        :return: vlan id as an integer
405        :rtype: integer
406        '''
407        vlan_id = None
408        if isinstance(network, vim.dvs.DistributedVirtualPortgroup):
409            vlan_id = network.config.defaultPortConfig.vlan.vlanId
410
411        if isinstance(network, vim.Network) and hasattr(network, 'host'):
412            for host in network.host:
413                for pg in host.config.network.portgroup:
414                    if pg.spec.name == network.name:
415                        vlan_id = pg.spec.vlanId
416                        return vlan_id
417
418        return vlan_id
419
420    def _get_nics_from_vm(self, vm_obj):
421        '''
422        return a list of dictionaries containing vm nic info and
423        a list of objects
424        :param vm_obj: object containing virtual machine
425        :return: list of dicts and list ith nic object(s)
426        :rtype: list, list
427        '''
428        nic_info_lst = []
429        nics = [nic for nic in vm_obj.config.hardware.device if isinstance(nic, vim.vm.device.VirtualEthernetCard)]
430        for nic in nics:
431            # common items of nic parameters
432            d_item = dict(
433                mac_address=nic.macAddress,
434                label=nic.deviceInfo.label,
435                unit_number=nic.unitNumber,
436                wake_onlan=nic.wakeOnLanEnabled,
437                allow_guest_ctl=nic.connectable.allowGuestControl,
438                connected=nic.connectable.connected,
439                start_connected=nic.connectable.startConnected,
440            )
441            # If a distributed port group specified
442            if isinstance(nic.backing, vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo):
443                key = nic.backing.port.portgroupKey
444                for portgroup in vm_obj.network:
445                    if hasattr(portgroup, 'key') and portgroup.key == key:
446                        d_item['network_name'] = portgroup.name
447                        d_item['switch'] = portgroup.config.distributedVirtualSwitch.name
448                        break
449            # If an NSX-T port group specified
450            elif isinstance(nic.backing, vim.vm.device.VirtualEthernetCard.OpaqueNetworkBackingInfo):
451                d_item['network_name'] = nic.backing.opaqueNetworkId
452                d_item['switch'] = nic.backing.opaqueNetworkType
453            # If a port group specified
454            elif isinstance(nic.backing, vim.vm.device.VirtualEthernetCard.NetworkBackingInfo):
455                d_item['network_name'] = nic.backing.network.name
456                d_item['vlan_id'] = self._get_vlanid_from_network(nic.backing.network)
457                if isinstance(nic.backing.network, vim.Network):
458                    for pg in vm_obj.runtime.host.config.network.portgroup:
459                        if pg.spec.name == nic.backing.network.name:
460                            d_item['switch'] = pg.spec.vswitchName
461                            break
462
463            for k in self.nic_device_type:
464                if isinstance(nic, self.nic_device_type[k]):
465                    d_item['device_type'] = k
466                    break
467
468            nic_info_lst.append(d_item)
469
470        nic_info_lst = sorted(nic_info_lst, key=lambda d: d['mac_address'] if (d['mac_address'] is not None) else '00:00:00:00:00:00')
471        return nic_info_lst, nics
472
473    def _get_compute_resource_by_name(self, recurse=True):
474        '''
475        get compute resource object with matching name of esxi_hostname or cluster
476        parameters.
477        :param recurse: recurse vmware content folder, default is True
478        :return: object matching vim.ComputeResource or None if no match
479        :rtype: object
480        '''
481        resource_name = None
482        if self.params['esxi_hostname']:
483            resource_name = self.params['esxi_hostname']
484
485        if self.params['cluster']:
486            resource_name = self.params['cluster']
487
488        container = self.content.viewManager.CreateContainerView(self.content.rootFolder, [vim.ComputeResource], recurse)
489        for obj in container.view:
490            if self.params['esxi_hostname'] and isinstance(obj, vim.ClusterComputeResource) and hasattr(obj, 'host'):
491                for host in obj.host:
492                    if host.name == resource_name:
493                        return obj
494
495            if obj.name == resource_name:
496                return obj
497
498        return None
499
500    def _new_nic_spec(self, vm_obj, nic_obj=None, network_params=None):
501        network = self._get_network_object(vm_obj, network_params)
502
503        if network_params:
504            connected = network_params['connected']
505            device_type = network_params['device_type'].lower()
506            directpath_io = network_params['directpath_io']
507            guest_control = network_params['guest_control']
508            label = network_params['label']
509            mac_address = network_params['mac_address']
510            start_connected = network_params['start_connected']
511            wake_onlan = network_params['wake_onlan']
512        else:
513            connected = self.params['connected']
514            device_type = self.params['device_type'].lower()
515            directpath_io = self.params['directpath_io']
516            guest_control = self.params['guest_control']
517            label = self.params['label']
518            mac_address = self.params['mac_address']
519            start_connected = self.params['start_connected']
520            wake_onlan = self.params['wake_onlan']
521
522        if not nic_obj:
523            device_obj = self.nic_device_type[device_type]
524            nic_spec = vim.vm.device.VirtualDeviceSpec(
525                device=device_obj()
526            )
527            if mac_address:
528                nic_spec.device.addressType = 'manual'
529                nic_spec.device.macAddress = mac_address
530
531            if label:
532                nic_spec.device.deviceInfo = vim.Description(
533                    label=label
534                )
535        else:
536            nic_spec = vim.vm.device.VirtualDeviceSpec(
537                operation=vim.vm.device.VirtualDeviceSpec.Operation.edit,
538                device=nic_obj
539            )
540            if label and label != nic_obj.deviceInfo.label:
541                nic_spec.device.deviceInfo = vim.Description(
542                    label=label
543                )
544            if mac_address and mac_address != nic_obj.macAddress:
545                nic_spec.device.addressType = 'manual'
546                nic_spec.device.macAddress = mac_address
547
548        nic_spec.device.backing = self._nic_backing_from_obj(network)
549        nic_spec.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo(
550            startConnected=start_connected,
551            allowGuestControl=guest_control,
552            connected=connected
553        )
554        nic_spec.device.wakeOnLanEnabled = wake_onlan
555
556        if directpath_io and not isinstance(nic_spec.device, vim.vm.device.VirtualVmxnet3):
557            self.module.fail_json(msg='directpath_io can only be used with the vmxnet3 device type')
558
559        if directpath_io and isinstance(nic_spec.device, vim.vm.device.VirtualVmxnet3):
560            nic_spec.device.uptCompatibilityEnabled = True
561        return nic_spec
562
563    def _nic_backing_from_obj(self, network_obj):
564        rv = None
565        if isinstance(network_obj, vim.dvs.DistributedVirtualPortgroup):
566            rv = vim.VirtualEthernetCardDistributedVirtualPortBackingInfo(
567                port=vim.DistributedVirtualSwitchPortConnection(
568                    portgroupKey=network_obj.key,
569                    switchUuid=network_obj.config.distributedVirtualSwitch.uuid
570                )
571            )
572        elif isinstance(network_obj, vim.OpaqueNetwork):
573            rv = vim.vm.device.VirtualEthernetCard.OpaqueNetworkBackingInfo(
574                opaqueNetworkType='nsx.LogicalSwitch',
575                opaqueNetworkId=network_obj.summary.opaqueNetworkId
576            )
577        elif isinstance(network_obj, vim.Network):
578            rv = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo(
579                deviceName=network_obj.name,
580                network=network_obj
581            )
582        return rv
583
584    def _nic_absent(self, network_params=None):
585        changed = False
586        diff = {'before': {}, 'after': {}}
587        if network_params:
588            mac_address = network_params['mac_address']
589        else:
590            mac_address = self.params['mac_address']
591
592        device_spec = None
593        vm_obj = self.get_vm()
594        if not vm_obj:
595            self.module.fail_json(msg='could not find vm: {0}'.format(self.params['name']))
596        nic_info, nic_obj_lst = self._get_nics_from_vm(vm_obj)
597
598        for nic in nic_info:
599            diff['before'].update({nic['mac_address']: copy.copy(nic)})
600
601        network_info = copy.deepcopy(nic_info)
602
603        for nic_obj in nic_obj_lst:
604            if nic_obj.macAddress == mac_address:
605                if self.module.check_mode:
606                    changed = True
607                    for nic in nic_info:
608                        if nic.get('mac_address') != nic_obj.macAddress:
609                            diff['after'].update({nic['mac_address']: copy.copy(nic)})
610                    network_info = [nic for nic in nic_info if nic.get('mac_address') != nic_obj.macAddress]
611                    return diff, changed, network_info
612                device_spec = vim.vm.device.VirtualDeviceSpec(
613                    device=nic_obj,
614                    operation=vim.vm.device.VirtualDeviceSpec.Operation.remove
615                )
616                break
617
618        if not device_spec:
619            diff['after'] = diff['before']
620            return diff, changed, network_info
621
622        try:
623            task = vm_obj.ReconfigVM_Task(vim.vm.ConfigSpec(deviceChange=[device_spec]))
624            wait_for_task(task)
625        except (vim.fault.InvalidDeviceSpec, vim.fault.RestrictedVersion) as e:
626            self.module.fail_json(msg='failed to reconfigure guest', detail=e.msg)
627
628        if task.info.state == 'error':
629            self.module.fail_json(msg='failed to reconfigure guest', detail=task.info.error.msg)
630
631        vm_obj = self.get_vm()
632        nic_info, nic_obj_lst = self._get_nics_from_vm(vm_obj)
633
634        for nic in nic_info:
635            diff['after'].update({nic.get('mac_address'): copy.copy(nic)})
636
637        network_info = nic_info
638        if diff['after'] != diff['before']:
639            changed = True
640
641        return diff, changed, network_info
642
643    def _get_nic_info(self):
644        rv = {'network_info': []}
645        vm_obj = self.get_vm()
646        nic_info, nic_obj_lst = self._get_nics_from_vm(vm_obj)
647
648        rv['network_info'] = nic_info
649        return rv
650
651    def _deprectated_list_config(self):
652        '''
653        this only exists to handle the old way of configuring interfaces, which
654        should be deprectated in favour of using loops in the playbook instead of
655        feeding lists directly into the module.
656        '''
657        diff = {'before': {}, 'after': {}}
658        changed = False
659        for i in self.params['networks']:
660            network_params = {}
661            network_params['mac_address'] = i.get('mac') or i.get('manual_mac')
662            network_params['network_name'] = i.get('name')
663            network_params['vlan_id'] = i.get('vlan')
664            network_params['switch'] = i.get('dvswitch_name')
665            network_params['guest_control'] = i.get('allow_guest_control', self.params['guest_control'])
666
667            for k in ['connected', 'device_type', 'directpath_io', 'force', 'label', 'start_connected', 'state', 'wake_onlan']:
668                network_params[k] = i.get(k, self.params[k])
669
670            if network_params['state'] in ['new', 'present']:
671                n_diff, n_changed, network_info = self._nic_present(network_params)
672                diff['before'].update(n_diff['before'])
673                diff['after'] = n_diff['after']
674                if n_changed:
675                    changed = True
676
677            if network_params['state'] == 'absent':
678                n_diff, n_changed, network_info = self._nic_absent(network_params)
679                diff['before'].update(n_diff['before'])
680                diff['after'] = n_diff['after']
681                if n_changed:
682                    changed = True
683
684        return diff, changed, network_info
685
686    def _nic_present(self, network_params=None):
687        changed = False
688        diff = {'before': {}, 'after': {}}
689        # backwards compatibility, clean up when params['networks']
690        # has been removed
691        if network_params:
692            force = network_params['force']
693            label = network_params['label']
694            mac_address = network_params['mac_address']
695            network_name = network_params['network_name']
696            switch = network_params['switch']
697            vlan_id = network_params['vlan_id']
698        else:
699            force = self.params['force']
700            label = self.params['label']
701            mac_address = self.params['mac_address']
702            network_name = self.params['network_name']
703            switch = self.params['switch']
704            vlan_id = self.params['vlan_id']
705
706        vm_obj = self.get_vm()
707        if not vm_obj:
708            self.module.fail_json(msg='could not find vm: {0}'.format(self.params['name']))
709
710        network_obj = self._get_network_object(vm_obj, network_params)
711        nic_info, nic_obj_lst = self._get_nics_from_vm(vm_obj)
712        label_lst = [d.get('label') for d in nic_info]
713        mac_addr_lst = [d.get('mac_address') for d in nic_info]
714        vlan_id_lst = [d.get('vlan_id') for d in nic_info]
715        network_name_lst = [d.get('network_name') for d in nic_info]
716
717        # TODO: make checks below less inelegant
718        if ((vlan_id in vlan_id_lst or network_name in network_name_lst)
719                and not mac_address
720                and not label
721                and not force):
722            for nic in nic_info:
723                diff['before'].update({nic.get('mac_address'): copy.copy(nic)})
724                diff['after'].update({nic.get('mac_address'): copy.copy(nic)})
725            return diff, changed, nic_info
726
727        if not network_obj and (network_name or vlan_id):
728            self.module.fail_json(
729                msg='unable to find specified network_name/vlan_id ({0}), check parameters'.format(
730                    network_name or vlan_id
731                )
732            )
733
734        for nic in nic_info:
735            diff['before'].update({nic.get('mac_address'): copy.copy(nic)})
736
737        if (mac_address and mac_address in mac_addr_lst) or (label and label in label_lst):
738            for nic_obj in nic_obj_lst:
739                if (mac_address and nic_obj.macAddress == mac_address) or (label and label == nic_obj.deviceInfo.label):
740                    device_spec = self._new_nic_spec(vm_obj, nic_obj, network_params)
741
742            # fabricate diff for check_mode
743            if self.module.check_mode:
744                for nic in nic_info:
745                    nic_mac = nic.get('mac_address')
746                    nic_label = nic.get('label')
747                    if nic_mac == mac_address or nic_label == label:
748                        diff['after'][nic_mac] = copy.deepcopy(nic)
749                        diff['after'][nic_mac].update({'switch': switch or nic['switch']})
750                        if network_obj:
751                            diff['after'][nic_mac].update(
752                                {
753                                    'vlan_id': self._get_vlanid_from_network(network_obj),
754                                    'network_name': network_obj.name
755                                }
756                            )
757                    else:
758                        diff['after'].update({nic_mac: copy.deepcopy(nic)})
759
760        if (not mac_address or mac_address not in mac_addr_lst) and (not label or label not in label_lst):
761            device_spec = self._new_nic_spec(vm_obj, None, network_params)
762            device_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
763            if self.module.check_mode:
764                # fabricate diff/returns for checkmode
765                diff['after'] = copy.deepcopy(diff['before'])
766                nic_mac = mac_address
767                if not nic_mac:
768                    nic_mac = 'AA:BB:CC:DD:EE:FF'
769                if not label:
770                    label = 'check_mode_adapter'
771                diff['after'].update(
772                    {
773                        nic_mac: {
774                            'vlan_id': self._get_vlanid_from_network(network_obj),
775                            'network_name': network_obj.name,
776                            'label': label,
777                            'mac_address': nic_mac,
778                            'unit_number': 40000
779                        }
780                    }
781                )
782
783        if self.module.check_mode:
784            network_info = [diff['after'][i] for i in diff['after']]
785            if diff['after'] != diff['before']:
786                changed = True
787            return diff, changed, network_info
788
789        if not self.module.check_mode:
790            try:
791                task = vm_obj.ReconfigVM_Task(vim.vm.ConfigSpec(deviceChange=[device_spec]))
792                wait_for_task(task)
793            except (vim.fault.InvalidDeviceSpec, vim.fault.RestrictedVersion) as e:
794                self.module.fail_json(msg='failed to reconfigure guest', detail=e.msg)
795
796            if task.info.state == 'error':
797                self.module.fail_json(msg='failed to reconfigure guest', detail=task.info.error.msg)
798
799            vm_obj = self.get_vm()
800            network_info, nic_obj_lst = self._get_nics_from_vm(vm_obj)
801            for nic in network_info:
802                diff['after'].update({nic.get('mac_address'): copy.copy(nic)})
803
804            if diff['after'] != diff['before']:
805                changed = True
806            return diff, changed, network_info
807
808
809def main():
810    argument_spec = vmware_argument_spec()
811    argument_spec.update(
812        name=dict(type='str'),
813        uuid=dict(type='str'),
814        use_instance_uuid=dict(type='bool', default=False),
815        moid=dict(type='str'),
816        folder=dict(type='str'),
817        datacenter=dict(type='str', default='ha-datacenter'),
818        esxi_hostname=dict(type='str'),
819        cluster=dict(type='str'),
820        mac_address=dict(type='str'),
821        vlan_id=dict(type='int'),
822        network_name=dict(type='str'),
823        device_type=dict(type='str', default='vmxnet3'),
824        label=dict(type='str'),
825        switch=dict(type='str'),
826        connected=dict(type='bool', default=True),
827        start_connected=dict(type='bool', default=True),
828        wake_onlan=dict(type='bool', default=False),
829        directpath_io=dict(type='bool', default=False),
830        force=dict(type='bool', default=False),
831        gather_network_info=dict(type='bool', default=False, aliases=['gather_network_facts']),
832        networks=dict(type='list', default=[], elements='dict'),
833        guest_control=dict(type='bool', default=True),
834        state=dict(type='str', default='present', choices=['absent', 'present'])
835    )
836
837    module = AnsibleModule(
838        argument_spec=argument_spec,
839        mutually_exclusive=[
840            ['vlan_id', 'network_name']
841        ],
842        required_one_of=[
843            ['name', 'uuid', 'moid']
844        ],
845        supports_check_mode=True
846    )
847
848    pyv = PyVmomiHelper(module)
849
850    if module.params['gather_network_info']:
851        nics = pyv._get_nic_info()
852        network_data = {}
853        nics_sorted = sorted(nics.get('network_info'), key=lambda k: k['unit_number'])
854        for n, i in enumerate(nics_sorted):
855            key_name = '{0}'.format(n)
856            network_data[key_name] = i
857            network_data[key_name].update({'mac_addr': i['mac_address'], 'name': i['network_name']})
858
859        module.exit_json(network_info=nics.get('network_info'), network_data=network_data, changed=False)
860
861    if module.params['networks']:
862        network_data = {}
863        module.deprecate(
864            msg='The old way of configuring interfaces by supplying an arbitrary list will be removed, loops should be used to handle multiple interfaces',
865            version='2.0.0',
866            collection_name='community.vmware'
867        )
868        diff, changed, network_info = pyv._deprectated_list_config()
869        nd = copy.deepcopy(network_info)
870        nics_sorted = sorted(nd, key=lambda k: k['unit_number'])
871        for n, i in enumerate(nics_sorted):
872            key_name = '{0}'.format(n)
873            network_data[key_name] = i
874            network_data[key_name].update({'mac_addr': i['mac_address'], 'name': i['network_name']})
875
876        module.exit_json(changed=changed, network_info=network_info, network_data=network_data, diff=diff)
877
878    if module.params['state'] == 'present':
879        diff, changed, network_info = pyv._nic_present()
880
881    if module.params['state'] == 'absent':
882        if not module.params['mac_address']:
883            module.fail_json(msg='parameter mac_address required when removing nics')
884        diff, changed, network_info = pyv._nic_absent()
885
886    module.exit_json(changed=changed, network_info=network_info, diff=diff)
887
888
889if __name__ == '__main__':
890    main()
891