Repository: ansible/ansible

Issue:
vmware_guest networks option using dict is problematic
<!---
Please verify first that your issue/request is not already reported in GitHub
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
vmware_guest

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
v2.3 (devel branch)

##### SUMMARY
<!--- Explain the problem briefly -->
The vmware_guest module requires a dict for the networks customizations, the example shows this:

```yaml
        networks:
          '192.168.1.0/24':
            network: 'VM Network'
            gateway: '192.168.1.1' 
```

Which is problematic if the **192.168.1.0/24** is variable, which in most cases will be.
Ansible does not allow to template the key-part of a dictionary.

My proposal is that we change this interface to use a normal list, there is no need to use a dictionary in this case.

```yaml
        networks:
        - name: 'VM Network'
          network: '192.168.1.0/24':
          gateway: '192.168.1.1' 
```

I have a patch available to make this change and would propose we fix this before this is being released to the public.


Discussion:
Hi @dagwieers 
Same bad experience here: I was going mad trying to understand how to pass the key of the dict as a variable. After some failed tries, i manged to make it work using the following syntax:

`networks: "{ '{{ net }}': {'gateway': '{{ gateway }}', 'network': '{{ vlan }}'}}"`

>  Ansible does not allow to template the key-part of a dictionary.

So, in the end, it's sufficient to express the dict in json notation and wrap all the line with double quotes

By the way, i agree with you that using a list would be a more simple way to define network settings. ;)
I have modified the module to use a list. But I am still refactoring other parts, so expect a fix once I am finished with that.
LGTM The biggest reason for the network in general was to ascertain the netmask. We really don't _need_ to verify the IP being assigned is a valid one for that subnet/VLAN. If its faster to change over and remove the network completely (and replace with the netmask) we can do that. 
cc @jctanner
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest.py b/lib/ansible/modules/cloud/vmware/vmware_guest.py
--- a/lib/ansible/modules/cloud/vmware/vmware_guest.py
+++ b/lib/ansible/modules/cloud/vmware/vmware_guest.py
@@ -147,9 +147,11 @@
         version_added: "2.3"
    networks:
         description:
-          - Network to use should include VM network name or VLAN, ip and gateway
-          - Add an optional field C(mac) to customize mac address
-          - Add an optional field C(domain) to configure a different dns domain on windows network interfaces
+          - Network to use should include C(name) or C(vlan) entry
+          - Add an optional C(ip) and C(netmask) for network configuration
+          - Add an optional C(gateway) entry to configure a gateway
+          - Add an optional C(mac) entry to customize mac address
+          - Add an optional C(dns_servers) or C(domain) entry per interface (Windows)
         required: False
         version_added: "2.3"
    snapshot_op:
@@ -207,9 +209,10 @@
         num_cpus: 1
         scsi: paravirtual
       networks:
-        '192.168.1.0/24':
-          network: VM Network
-          mac: 'aa:bb:dd:aa:00:14'
+      - network: VM Network
+        ip: 192.168.1.100
+        netmask: 255.255.255.0
+        mac: 'aa:bb:dd:aa:00:14'
       template: template_el7
       wait_for_ip_address: yes
     register: deploy
@@ -251,15 +254,15 @@
       name: testvm-2
       template: template_windows
       networks:
-        '192.168.1.0/24':
-          network: VM Network
-          gateway: 192.168.1.1
-          ip: 192.168.1.100
-          mac: 'aa:bb:dd:aa:00:14'
-          domain: my_domain
-          dns_servers:
-          - 192.168.1.1
-          - 192.168.1.2
+      - name: VM Network
+        ip: 192.168.1.100
+        netmask: 255.255.255.0
+        gateway: 192.168.1.1
+        mac: 'aa:bb:dd:aa:00:14'
+        domain: my_domain
+        dns_servers:
+        - 192.168.1.1
+        - 192.168.1.2
       customization:
         autologon: True
         dns_servers:
@@ -354,7 +357,6 @@
 
 import os
 import time
-from netaddr import IPNetwork, IPAddress
 
 # import module snippets
 from ansible.module_utils.basic import AnsibleModule
@@ -460,13 +462,13 @@ def create_nic(self, device_type, device_label, device_infos):
             nic.device = vim.vm.device.VirtualSriovEthernetCard()
         else:
             self.module.fail_json(msg="Invalid device_type '%s' for network %s" %
-                                      (device_type, device_infos['network']))
+                                      (device_type, device_infos['name']))
 
         nic.device.wakeOnLanEnabled = True
         nic.device.addressType = 'assigned'
         nic.device.deviceInfo = vim.Description()
         nic.device.deviceInfo.label = device_label
-        nic.device.deviceInfo.summary = device_infos['network']
+        nic.device.deviceInfo.summary = device_infos['name']
         nic.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo()
         nic.device.connectable.startConnected = True
         nic.device.connectable.allowGuestControl = True
@@ -689,7 +691,7 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None, cache=False)
                         matches.append(thisvm)
                     if len(matches) > 1:
                         self.module.fail_json(
-                            msg='more than 1 vm exists by the name %s. Please specify a uuid, or a folder, '
+                            msg='More than 1 VM exists by the name %s. Please specify a uuid, or a folder, '
                                 'or a datacenter or name_match' % name)
                     if matches:
                         vm = matches[0]
@@ -883,33 +885,29 @@ def configure_network(self, vm_obj):
 
         network_devices = list()
         for network in self.params['networks']:
-            if network:
-                if 'ip' in self.params['networks'][network]:
-                    ip = self.params['networks'][network]['ip']
-                    if ip not in IPNetwork(network):
-                        self.module.fail_json(msg="ip '%s' not in network %s" % (ip, network))
-
-                    ipnet = IPNetwork(network)
-                    self.params['networks'][network]['subnet_mask'] = str(ipnet.netmask)
-
-                if 'network' in self.params['networks'][network]:
-                    if get_obj(self.content, [vim.Network], self.params['networks'][network]['network']) is None:
-                        self.module.fail_json(msg="Network %s doesn't exists" % network)
-                elif 'vlan' in self.params['networks'][network]:
-                    network_name = None
-                    dvps = get_all_objs(self.content, [vim.dvs.DistributedVirtualPortgroup])
-                    for dvp in dvps:
-                        if dvp.config.defaultPortConfig.vlan.vlanId == self.params['networks'][network]['vlan']:
-                            network_name = dvp.config.name
-                            break
-                    if network_name:
-                        self.params['networks'][network]['network'] = network_name
-                    else:
-                        self.module.fail_json(msg="VLAN %(vlan)s doesn't exists" % self.params['networks'][network])
+            if 'ip' in network or 'netmask' in network:
+                if 'ip' not in network or not 'netmask' in network:
+                    self.module.fail_json(msg="Both 'ip' and 'netmask' are required together.")
+
+            if 'name' in network:
+                if get_obj(self.content, [vim.Network], network['name']) is None:
+                    self.module.fail_json(msg="Network '%(name)s' does not exists" % network)
+
+            elif 'vlan' in network:
+                dvps = get_all_objs(self.content, [vim.dvs.DistributedVirtualPortgroup])
+                for dvp in dvps:
+                    if hasattr(dvp.config.defaultPortConfig, 'vlan') and dvp.config.defaultPortConfig.vlan.vlanId == 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="You need to define a network or a vlan")
+                    self.module.fail_json(msg="VLAN '%(vlan)s' does not exist" % network)
+            else:
+                self.module.fail_json(msg="You need to define a network name or a vlan")
 
-                network_devices.append(self.params['networks'][network])
+            network_devices.append(network)
 
         # List current device for Clone or Idempotency
         current_net_devices = self.get_vm_network_interfaces(vm=vm_obj)
@@ -939,9 +937,9 @@ def configure_network(self, vm_obj):
                 nic.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
                 nic_change_detected = True
 
-            if hasattr(self.cache.get_network(network_devices[key]['network']), 'portKeys'):
+            if hasattr(self.cache.get_network(network_devices[key]['name']), 'portKeys'):
                 # VDS switch
-                pg_obj = get_obj(self.content, [vim.dvs.DistributedVirtualPortgroup], network_devices[key]['network'])
+                pg_obj = get_obj(self.content, [vim.dvs.DistributedVirtualPortgroup], network_devices[key]['name'])
                 dvs_port_connection = vim.dvs.PortConnection()
                 dvs_port_connection.portgroupKey = pg_obj.key
                 dvs_port_connection.switchUuid = pg_obj.config.distributedVirtualSwitch.uuid
@@ -953,13 +951,13 @@ def configure_network(self, vm_obj):
                     nic.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo()
                     nic_change_detected = True
 
-                net_obj = self.cache.get_network(network_devices[key]['network'])
+                net_obj = self.cache.get_network(network_devices[key]['name'])
                 if nic.device.backing.network != net_obj:
                     nic.device.backing.network = net_obj
                     nic_change_detected = True
 
-                if nic.device.backing.deviceName != network_devices[key]['network']:
-                    nic.device.backing.deviceName = network_devices[key]['network']
+                if nic.device.backing.deviceName != network_devices[key]['name']:
+                    nic.device.backing.deviceName = network_devices[key]['name']
                     nic_change_detected = True
 
             if nic_change_detected:
@@ -988,30 +986,29 @@ def customize_customvalues(self, vm_obj):
     def customize_vm(self, vm_obj):
         # Network settings
         adaptermaps = []
-        if len(self.params['networks']) > 0:
-            for network in self.params['networks']:
-                if 'ip' in self.params['networks'][network]:
-                    guest_map = vim.vm.customization.AdapterMapping()
-                    guest_map.adapter = vim.vm.customization.IPSettings()
-                    guest_map.adapter.ip = vim.vm.customization.FixedIp()
-                    guest_map.adapter.ip.ipAddress = str(self.params['networks'][network]['ip'])
-                    guest_map.adapter.subnetMask = str(self.params['networks'][network]['subnet_mask'])
-
-                    if 'gateway' in self.params['networks'][network]:
-                        guest_map.adapter.gateway = self.params['networks'][network]['gateway']
-
-                    # On Windows, DNS domain and DNS servers can be set by network interface
-                    # https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.customization.IPSettings.html
-                    if 'domain' in self.params['networks'][network]:
-                        guest_map.adapter.dnsDomain = self.params['networks'][network]['domain']
-                    elif self.params['customization'].get('domain'):
-                        guest_map.adapter.dnsDomain = self.params['customization']['domain']
-                    if 'dns_servers' in self.params['networks'][network]:
-                        guest_map.adapter.dnsServerList = self.params['networks'][network]['dns_servers']
-                    elif self.params['customization'].get('dns_servers'):
-                        guest_map.adapter.dnsServerList = self.params['customization']['dns_servers']
-
-                    adaptermaps.append(guest_map)
+        for network in self.params['networks']:
+            if 'ip' in network and 'netmask' in network:
+                guest_map = vim.vm.customization.AdapterMapping()
+                guest_map.adapter = vim.vm.customization.IPSettings()
+                guest_map.adapter.ip = vim.vm.customization.FixedIp()
+                guest_map.adapter.ip.ipAddress = str(network['ip'])
+                guest_map.adapter.subnetMask = str(network['netmask'])
+
+                if 'gateway' in network:
+                    guest_map.adapter.gateway = network['gateway']
+
+                # On Windows, DNS domain and DNS servers can be set by network interface
+                # https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.customization.IPSettings.html
+                if 'domain' in network:
+                    guest_map.adapter.dnsDomain = network['domain']
+                elif self.params['customization'].get('domain'):
+                    guest_map.adapter.dnsDomain = self.params['customization']['domain']
+                if 'dns_servers' in network:
+                    guest_map.adapter.dnsServerList = network['dns_servers']
+                elif self.params['customization'].get('dns_servers'):
+                    guest_map.adapter.dnsServerList = self.params['customization']['dns_servers']
+
+                adaptermaps.append(guest_map)
 
         # Global DNS settings
         globalip = vim.vm.customization.GlobalIPSettings()
@@ -1791,7 +1788,7 @@ def main():
             esxi_hostname=dict(required=False, type='str', default=None),
             cluster=dict(required=False, type='str', default=None),
             wait_for_ip_address=dict(required=False, type='bool', default=True),
-            networks=dict(required=False, type='dict', default={}),
+            networks=dict(required=False, type='list', default=[]),
             resource_pool=dict(required=False, type='str', default=None),
             customization=dict(required=False, type='dict', no_log=True, default={}),
         ),


Test patch: