Repository: ansible/ansible

Issue:
Missing 'port' AttributeError when modifying existing VM network
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
vmware_guest.py

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
$ /home/sstrattn/ansible/bin/ansible --version
ansible 2.3.2.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/sstrattn/ansible/lib/ansible/modules']
  python version = 2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609]
```

##### CONFIGURATION
<!---
If using Ansible 2.4 or above, paste the results of "ansible-config dump --only-changed"

Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).

-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->

N/A

##### SUMMARY
<!--- Explain the problem briefly -->
When attempting to modify the portgroup assignment of an existing VM, vmware_guest gives an error:
AttributeError: 'vim.vm.device.VirtualEthernetCard.NetworkBackingIn' object has no attribute 'port'
##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->
<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: Move myvm to new EPG
        vmware_guest:
           hostname: "{{ vcenter }}"
           username: "{{ vcenterid }}"
           password: "{{ vcenterpwd }}"
           validate_certs: false
           datacenter: mydatacenter
           folder: myvmfolder
           name: myvmname
           uuid: myuuid
           networks:
            - name: newportgroupadded
            - name: existingnetworkunchanged
           state: present
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Expected the VM network 1 to move to 'newportgroupadded'

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
The full traceback is:
Traceback (most recent call last):
  File "/tmp/ansible_n6hEeJ/ansible_module_vmware_guest.py", line 1503, in <module>
    main()
  File "/tmp/ansible_n6hEeJ/ansible_module_vmware_guest.py", line 1479, in main
    result = pyv.reconfigure_vm()
  File "/tmp/ansible_n6hEeJ/ansible_module_vmware_guest.py", line 1351, in reconfigure_vm
    self.configure_network(vm_obj=self.current_vm_obj)
  File "/tmp/ansible_n6hEeJ/ansible_module_vmware_guest.py", line 727, in configure_network
    (nic.device.backing.port.portgroupKey != pg_obj.key or
AttributeError: 'vim.vm.device.VirtualEthernetCard.NetworkBackingIn' object has no attribute 'port'

fatal: [localhost]: FAILED! => {
    "changed": false, 
    "failed": true, 
    "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_n6hEeJ/ansible_module_vmware_guest.py\", line 1503, in <module>\n    main()\n  File \"/tmp/ansible_n6hEeJ/ansible_module_vmware_guest.py\", line 1479, in main\n    result = pyv.reconfigure_vm()\n  File \"/tmp/ansible_n6hEeJ/ansible_module_vmware_guest.py\", line 1351, in reconfigure_vm\n    self.configure_network(vm_obj=self.current_vm_obj)\n  File \"/tmp/ansible_n6hEeJ/ansible_module_vmware_guest.py\", line 727, in configure_network\n    (nic.device.backing.port.portgroupKey != pg_obj.key or\nAttributeError: 'vim.vm.device.VirtualEthernetCard.NetworkBackingIn' object has no attribute 'port'\n", 
    "module_stdout": "", 
    "msg": "MODULE FAILURE", 
    "rc": 0
}
```



Discussion:
cc @akasurde @dagwieers @dav1x @jctanner @nerzhul
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Because the version Ansible is showing may not be completely accurate, as I had performed a 'git clone' in order to get some of the later features, like the ACI modules, I decided to start with a clean install. So I made a Docker container that has the latest devel branch (as of 30 minutes ago) and all of the necessary components, and ran the playbook again. Same error.
```
# ansible --version
ansible 2.5.0
  config file = None
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python2.7/dist-packages/ansible-2.5.0-py2.7.egg/ansible
  executable location = /usr/local/bin/ansible
  python version = 2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609]
```

```
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: AttributeError: 'vim.vm.device.VirtualEthernetCard.NetworkBackingIn' object has no attribute 'port'
fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_kpLFaT/ansible_module_vmware_guest.py\", line 1503, in <module>\n    main()\n  File \"/tmp/ansible_kpLFaT/ansible_module_vmware_guest.py\", line 1479, in main\n    result = pyv.reconfigure_vm()\n  File \"/tmp/ansible_kpLFaT/ansible_module_vmware_guest.py\", line 1351, in reconfigure_vm\n    self.configure_network(vm_obj=self.current_vm_obj)\n  File \"/tmp/ansible_kpLFaT/ansible_module_vmware_guest.py\", line 727, in configure_network\n    (nic.device.backing.port.portgroupKey != pg_obj.key or\nAttributeError: 'vim.vm.device.VirtualEthernetCard.NetworkBackingIn' object has no attribute 'port'\n", "module_stdout": "", "msg": "MODULE FAILURE", "rc": 0}
```

Curious does it work if you reverse the order? What looks to be happening in the module code is that it's checking whether the NEW "Network 1" has a portKeys attribute (DVS) and then trying to pull a port attribute from the EXISTING "Network 1".

With `network_devices` coming from the passed-in module params and `current_net_devices` obviously coming from existing state...
Line 701 (enclosing loop -- using same index for both new and existing devices):
```
        for key in range(0, len(network_devices)):
```
Line 716:
```
                nic.device = current_net_devices[key]
```
Lines 722-727:
```
            if hasattr(self.cache.get_network(network_devices[key]['name']), 'portKeys'):
                # VDS switch
                pg_obj = find_obj(self.content, [vim.dvs.DistributedVirtualPortgroup], network_devices[key]['name'])

                if (nic.device.backing and
                        (nic.device.backing.port.portgroupKey != pg_obj.key or
```

Is the underlying issue that if the new network is associated with a VDS, the module assumes the existing network is also associated with a VDS? The existing network in my case is merely "VM Network", the default 'standard network'.

As for 'reversing the order', I don't feel comfortable/qualified in modifying the module code myself, but I'm happy to test any changes recommended, so long as the specific code changes are given.
Yes that's how it appears to me. I'm pretty green on VMware networking and not suggesting you reverse the order to fix it -- I'm suggesting that this error wouldn't occur for the reason you just described. And I mean reversing the order of networks in the playbook, for clarity.

I'll leave the fix to someone who better understands the implications of changes to this method :)
And hey, you can always checkout and modify the module code on your own box until you get it right and can submit upstream, if you have a VMware environment you don't mind screwing up.
I do have a test environment and I'd be willing to mess with the code, but it will take a bit of time for me to become familiar enough with the code to make changes without worrying I'll break something else in the process. But it might be as simple as checking hasAttr for the 'port' attribute first - if the existing network has it but the new one does not, then ```net_change_detected = True```.

In the meantime, I did a successful test of the playbook by first assigning the VMs to another DVS portgroup. So DVS to DVS works fine, it's Standard to DVS that causes the error.
Yeah at first glance it looks like changing
```
                if (nic.device.backing and
                        (nic.device.backing.port.portgroupKey != pg_obj.key or
                         nic.device.backing.port.switchUuid != pg_obj.config.distributedVirtualSwitch.uuid)):
                    nic_change_detected = True
```
to
```
                if (nic.device.backing and
                        (not hasattr(nic.device.backing, 'port') or
                         nic.device.backing.port.portgroupKey != pg_obj.key or
                         nic.device.backing.port.switchUuid != pg_obj.config.distributedVirtualSwitch.uuid)):
                    nic_change_detected = True
```
would take care of the immediate issue.

It looks like the corresponding `else` block for a standard vSwitch handles it a bit differently with an `isinstance` call:
```
            else:
                # vSwitch
                if not isinstance(nic.device.backing, vim.vm.device.VirtualEthernetCard.NetworkBackingInfo):
                    nic.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo()
                    nic_change_detected = True
```

I lean a bit more toward an `isinstance` and some minor restructuring, for greater clarity of intent.

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
@@ -629,9 +629,10 @@ def configure_network(self, vm_obj):
                 # VDS switch
                 pg_obj = find_obj(self.content, [vim.dvs.DistributedVirtualPortgroup], network_devices[key]['name'])
 
-                if (nic.device.backing and
-                        (nic.device.backing.port.portgroupKey != pg_obj.key or
-                         nic.device.backing.port.switchUuid != pg_obj.config.distributedVirtualSwitch.uuid)):
+                if (nic.device.backing and not hasattr(nic.device.backing, 'port')):
+                    nic_change_detected = True
+                elif (nic.device.backing and (nic.device.backing.port.portgroupKey != pg_obj.key or
+                      nic.device.backing.port.switchUuid != pg_obj.config.distributedVirtualSwitch.uuid)):
                     nic_change_detected = True
 
                 dvs_port_connection = vim.dvs.PortConnection()


Test patch: