Repository: ansible-collections/community.vmware

Issue:
community.vmware.vmware_guest shows changed in check mode
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Explain the problem briefly below -->
When changing powerstate and creating a VM on ESXI, the module shows changed on check mode
##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
community.vmware.vmware_guest 
##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```paste below
ansible [core 2.11.5] 
  python version = 3.8.6 (default, Jan 22 2021, 11:41:28) [GCC 8.4.1 20200928 (Red Hat 8.4.1-1)]
  jinja version = 3.0.2
  libyaml = True
```

##### COLLECTION VERSION
<!--- Paste verbatim output from "ansible-galaxy collection list <namespace>.<collection>"  between the quotes
for example: ansible-galaxy collection list community.general
-->
```paste below
ansible-galaxy collection list community.vmware
# /usr/share/ansible/collections/ansible_collections
Collection       Version
---------------- -------
community.vmware 1.15.0 
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```paste below
ANSIBLE_PIPELINING(/playbooks/ansible.cfg) = True
BECOME_ALLOW_SAME_USER(/playbooks/ansible.cfg) = True
BECOME_PLUGIN_PATH(/playbooks/ansible.cfg) = ['/playbooks/collections/ansible_collections/utils/plugins/become', '/usr/share/ansible/co>
CALLBACKS_ENABLED(/playbooks/ansible.cfg) = ['utils.log', 'utils.profile_tasks_custom']
DEFAULT_ACTION_PLUGIN_PATH(/playbooks/ansible.cfg) = ['/playbooks/collections/ansible_collections/utils/plugins/action', '/usr/share/an>
DEFAULT_FILTER_PLUGIN_PATH(/playbooks/ansible.cfg) = ['/playbooks/collections/ansible_collections/utils/plugins/filter', '/usr/share/an>
DEFAULT_FORKS(/playbooks/ansible.cfg) = 100
DEFAULT_HOST_LIST(/playbooks/ansible.cfg) = ['/playbooks/inventory.yml']
DEFAULT_LOAD_CALLBACK_PLUGINS(/playbooks/ansible.cfg) = True
DEFAULT_LOCAL_TMP(/playbooks/ansible.cfg) = /tmp/.ansible/ansible-local-1610062bwg_4fe
DEFAULT_MODULE_PATH(/playbooks/ansible.cfg) = ['/playbooks/collections/ansible_collections/utils/plugins/modules', '/usr/share/ansible/>
DEFAULT_ROLES_PATH(/playbooks/ansible.cfg) = ['/playbooks/roles', '/usr/share/ansible/roles', '/.ansible/roles', '/usr/sh>
DEFAULT_STDOUT_CALLBACK(/playbooks/ansible.cfg) = utils.yaml_custom
DEFAULT_STRATEGY(/playbooks/ansible.cfg) = free
DEFAULT_TEST_PLUGIN_PATH(/playbooks/ansible.cfg) = ['/playbooks/collections/ansible_collections/utils/plugins/test', '/usr/share/ansibl>
DEFAULT_TIMEOUT(/playbooks/ansible.cfg) = 60
DEFAULT_VAULT_PASSWORD_FILE(/playbooks/ansible.cfg) = /playbooks/key
DIFF_ALWAYS(/playbooks/ansible.cfg) = True
DIFF_CONTEXT(/playbooks/ansible.cfg) = 0
DISPLAY_SKIPPED_HOSTS(/playbooks/ansible.cfg) = False
DUPLICATE_YAML_DICT_KEY(/playbooks/ansible.cfg) = ignore
HOST_KEY_CHECKING(/playbooks/ansible.cfg) = False
INTERPRETER_PYTHON(/playbooks/ansible.cfg) = auto_silent
INVENTORY_CACHE_ENABLED(/playbooks/ansible.cfg) = True
INVENTORY_ENABLED(/playbooks/ansible.cfg) = ['auto', 'host_list', 'script', 'ini', 'yaml']
LOCALHOST_WARNING(/playbooks/ansible.cfg) = False
MAX_FILE_SIZE_FOR_DIFF(/playbooks/ansible.cfg) = 1048576
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
RHEL8.4 

##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->
Create an existing VM on ESXI using the module, then run the same code in check mode

<!--- Paste example playbooks or commands between quotes below -->
```yaml
# Different task that has same issue
    - name: Apply power state of {{ create_vm_state }} to {{ vm_name }} on {{ esxi_host }}
      community.vmware.vmware_guest:
        datacenter: "{{ esxi_datacenter }}"
        name: "{{ vm_name }}"
        state: "{{ create_vm_state }}"
      delegate_to: localhost
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
Task shouldn't change in check mode when the VM has already been created previously. Running it outside of check mode doesn't show changed

##### ACTUAL RESULTS
The task mentioned above change in check mode

<!--- Paste verbatim command output between quotes -->
```paste below
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->
TASK [Create_VM: Apply power state of poweredon to VM on ESXI HOST] ****************************************************************************************************************************
changed: [VM -> localhost]
Tuesday 29 March 2022  14:06:31 +0000 (0:00:03.977)       0:01:40.410 ********* 
Tuesday 29 March 2022  14:06:31 +0000 (0:00:00.062)       0:01:40.473 ********* 
```



Discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
It looks like the module simply sets `changed=True` when run in check mode:

https://github.com/ansible-collections/community.vmware/blob/30de1f007ae3d00c8217129eddea66b5a4a7c004/plugins/modules/vmware_guest.py#L3387-L3396

https://github.com/ansible-collections/community.vmware/blob/30de1f007ae3d00c8217129eddea66b5a4a7c004/plugins/modules/vmware_guest.py#L3401-L3408

https://github.com/ansible-collections/community.vmware/blob/30de1f007ae3d00c8217129eddea66b5a4a7c004/plugins/modules/vmware_guest.py#L3410-L3421

https://github.com/ansible-collections/community.vmware/blob/30de1f007ae3d00c8217129eddea66b5a4a7c004/plugins/modules/vmware_guest.py#L3442-L3449

I think that changing this won't be easy.
Usually the paradigm is, right before any actual change is made, module.check_mode is checked, at the lowest granularity for any change.
By saying that it won't be easy, does that mean it won't be done?  

I agree with the Author that check mode should accurately reflect if it would change or be OK instead of just blindly reporting Changed.
> By saying that it won't be easy, does that mean it won't be done?

Not necessarily. But this is a community collection and someone from the community needs to find enough time to invest fixing this. And I don't know when (or if) this will be the case. Actually, I think it might need a major refactoring of the module. Which doesn't make it easier to find enough time to work on this.

> I agree with the Author that check mode should accurately reflect if it would change or be OK instead of just blindly reporting Changed.

I fully agree, too. But, as it is, we're short-staffed and additionally... well, see above.
I will try to work this

Resolving patch:
diff --git a/plugins/modules/vmware_guest.py b/plugins/modules/vmware_guest.py
--- a/plugins/modules/vmware_guest.py
+++ b/plugins/modules/vmware_guest.py
@@ -1194,6 +1194,7 @@ def __init__(self, module):
         self.relospec = None
         self.change_detected = False  # a change was detected and needs to be applied through reconfiguration
         self.change_applied = False   # a change was applied meaning at least one task succeeded
+        self.tracked_changes = {}     # dict of changes made or would-be-made in check mode, updated when change_applied is set
         self.customspec = None
         self.cache = PyVmomiCache(self.content, dc_name=self.params['datacenter'])
 
@@ -1323,7 +1324,8 @@ def configure_cpu_and_memory(self, vm_obj, vm_creation=False):
         num_cpus = self.params['hardware']['num_cpus']
         if num_cpus is not None:
             # check VM power state and cpu hot-add/hot-remove state before re-config VM
-            if vm_obj and vm_obj.runtime.powerState == vim.VirtualMachinePowerState.poweredOn:
+            # Allow VM to be powered on during this check when in check mode, when no changes will actually be made
+            if vm_obj and vm_obj.runtime.powerState == vim.VirtualMachinePowerState.poweredOn and not self.module.check_mode:
                 if not vm_obj.config.cpuHotRemoveEnabled and num_cpus < vm_obj.config.hardware.numCPU:
                     self.module.fail_json(msg="Configured cpu number is less than the cpu number of the VM, "
                                               "cpuHotRemove is not enabled")
@@ -1336,12 +1338,12 @@ def configure_cpu_and_memory(self, vm_obj, vm_creation=False):
                 if num_cpus % num_cpu_cores_per_socket != 0:
                     self.module.fail_json(msg="hardware.num_cpus attribute should be a multiple "
                                               "of hardware.num_cpu_cores_per_socket")
-                self.configspec.numCoresPerSocket = num_cpu_cores_per_socket
-                if vm_obj is None or self.configspec.numCoresPerSocket != vm_obj.config.hardware.numCoresPerSocket:
+                if vm_obj is None or num_cpu_cores_per_socket != vm_obj.config.hardware.numCoresPerSocket:
                     self.change_detected = True
-            self.configspec.numCPUs = num_cpus
-            if vm_obj is None or self.configspec.numCPUs != vm_obj.config.hardware.numCPU:
+                    self.configspec.numCoresPerSocket = num_cpu_cores_per_socket
+            if vm_obj is None or num_cpus != vm_obj.config.hardware.numCPU:
                 self.change_detected = True
+                self.configspec.numCPUs = num_cpus
         # num_cpu is mandatory for VM creation
         elif vm_creation and not self.params['template']:
             self.module.fail_json(msg="hardware.num_cpus attribute is mandatory for VM creation")
@@ -1353,47 +1355,51 @@ def configure_cpu_and_memory(self, vm_obj, vm_creation=False):
                 if vm_obj.config.memoryHotAddEnabled and memory_mb < vm_obj.config.hardware.memoryMB:
                     self.module.fail_json(msg="Configured memory is less than memory size of the VM, "
                                               "operation is not supported")
-                elif not vm_obj.config.memoryHotAddEnabled and memory_mb != vm_obj.config.hardware.memoryMB:
+                # Allow VM to be powered on during this check when in check mode, when no changes will actually be made
+                elif not vm_obj.config.memoryHotAddEnabled and memory_mb != vm_obj.config.hardware.memoryMB and not self.module.check_mode:
                     self.module.fail_json(msg="memoryHotAdd is not enabled")
-            self.configspec.memoryMB = memory_mb
-            if vm_obj is None or self.configspec.memoryMB != vm_obj.config.hardware.memoryMB:
+            if vm_obj is None or memory_mb != vm_obj.config.hardware.memoryMB:
                 self.change_detected = True
+                self.configspec.memoryMB = memory_mb
         # memory_mb is mandatory for VM creation
         elif vm_creation and not self.params['template']:
             self.module.fail_json(msg="hardware.memory_mb attribute is mandatory for VM creation")
 
         hotadd_memory = self.params['hardware']['hotadd_memory']
         if hotadd_memory is not None:
+            # Allow VM to be powered on during this check when in check mode, when no changes will actually be made
             if vm_obj and vm_obj.runtime.powerState == vim.VirtualMachinePowerState.poweredOn and \
-                    vm_obj.config.memoryHotAddEnabled != hotadd_memory:
+                    vm_obj.config.memoryHotAddEnabled != hotadd_memory and not self.module.check_mode:
                 self.module.fail_json(msg="Configure hotadd memory operation is not supported when VM is power on")
-            self.configspec.memoryHotAddEnabled = hotadd_memory
-            if vm_obj is None or self.configspec.memoryHotAddEnabled != vm_obj.config.memoryHotAddEnabled:
+            if vm_obj is None or hotadd_memory != vm_obj.config.memoryHotAddEnabled:
                 self.change_detected = True
+                self.configspec.memoryHotAddEnabled = hotadd_memory
 
         hotadd_cpu = self.params['hardware']['hotadd_cpu']
         if hotadd_cpu is not None:
+            # Allow VM to be powered on during this check when in check mode, when no changes will actually be made
             if vm_obj and vm_obj.runtime.powerState == vim.VirtualMachinePowerState.poweredOn and \
-                    vm_obj.config.cpuHotAddEnabled != hotadd_cpu:
+                    vm_obj.config.cpuHotAddEnabled != hotadd_cpu and not self.module.check_mode:
                 self.module.fail_json(msg="Configure hotadd cpu operation is not supported when VM is power on")
-            self.configspec.cpuHotAddEnabled = hotadd_cpu
-            if vm_obj is None or self.configspec.cpuHotAddEnabled != vm_obj.config.cpuHotAddEnabled:
+            if vm_obj is None or hotadd_cpu != vm_obj.config.cpuHotAddEnabled:
                 self.change_detected = True
+                self.configspec.cpuHotAddEnabled = hotadd_cpu
 
         hotremove_cpu = self.params['hardware']['hotremove_cpu']
         if hotremove_cpu is not None:
+            # Allow VM to be powered on during this check when in check mode, when no changes will actually be made
             if vm_obj and vm_obj.runtime.powerState == vim.VirtualMachinePowerState.poweredOn and \
-                    vm_obj.config.cpuHotRemoveEnabled != hotremove_cpu:
+                    vm_obj.config.cpuHotRemoveEnabled != hotremove_cpu and not self.module.check_mode:
                 self.module.fail_json(msg="Configure hotremove cpu operation is not supported when VM is power on")
-            self.configspec.cpuHotRemoveEnabled = hotremove_cpu
-            if vm_obj is None or self.configspec.cpuHotRemoveEnabled != vm_obj.config.cpuHotRemoveEnabled:
+            if vm_obj is None or hotremove_cpu != vm_obj.config.cpuHotRemoveEnabled:
                 self.change_detected = True
+                self.configspec.cpuHotRemoveEnabled = hotremove_cpu
 
         memory_reservation_lock = self.params['hardware']['memory_reservation_lock']
         if memory_reservation_lock is not None:
-            self.configspec.memoryReservationLockedToMax = memory_reservation_lock
-            if vm_obj is None or self.configspec.memoryReservationLockedToMax != vm_obj.config.memoryReservationLockedToMax:
+            if vm_obj is None or memory_reservation_lock != vm_obj.config.memoryReservationLockedToMax:
                 self.change_detected = True
+                self.configspec.memoryReservationLockedToMax = memory_reservation_lock
 
         vpmc_enabled = self.params['hardware']['vpmc_enabled']
         if vpmc_enabled is not None:
@@ -1580,8 +1586,9 @@ def configure_cdrom_list(self, vm_obj):
                         break
                 # create new CD-ROM
                 if not cdrom_device and cdrom.get('state') != 'absent':
+                    # Allow VM to be powered on during this check when in check mode, when no changes will actually be made
                     if vm_obj and vm_obj.runtime.powerState == vim.VirtualMachinePowerState.poweredOn and \
-                            isinstance(ctl_device, vim.vm.device.VirtualIDEController):
+                            isinstance(ctl_device, vim.vm.device.VirtualIDEController) and not self.module.check_mode:
                         self.module.fail_json(msg='CD-ROM attach to IDE controller not support hot-add.')
                     if len(ctl_device.device) == 2 and isinstance(ctl_device, vim.vm.device.VirtualIDEController):
                         self.module.fail_json(msg='Maximum number of CD-ROMs attached to IDE controller is 2.')
@@ -1604,8 +1611,9 @@ def configure_cdrom_list(self, vm_obj):
                     self.configspec.deviceChange.append(cdrom_spec)
                 # delete CD-ROM
                 elif cdrom_device and cdrom.get('state') == 'absent':
+                    # Allow VM to be powered on during this check when in check mode, when no changes will actually be made
                     if vm_obj and vm_obj.runtime.powerState != vim.VirtualMachinePowerState.poweredOff and \
-                            isinstance(ctl_device, vim.vm.device.VirtualIDEController):
+                            isinstance(ctl_device, vim.vm.device.VirtualIDEController) and not self.module.check_mode:
                         self.module.fail_json(msg='CD-ROM attach to IDE controller not support hot-remove.')
                     cdrom_spec = self.device_helper.remove_cdrom(cdrom_device)
                     self.change_detected = True
@@ -1619,29 +1627,25 @@ def configure_hardware_params(self, vm_obj):
         """
         max_connections = self.params['hardware']['max_connections']
         if max_connections is not None:
-            self.configspec.maxMksConnections = max_connections
-            if vm_obj is None or self.configspec.maxMksConnections != vm_obj.config.maxMksConnections:
+            if vm_obj is None or max_connections != vm_obj.config.maxMksConnections:
                 self.change_detected = True
+                self.configspec.maxMksConnections = max_connections
 
         nested_virt = self.params['hardware']['nested_virt']
         if nested_virt is not None:
-            self.configspec.nestedHVEnabled = nested_virt
-            if vm_obj is None or self.configspec.nestedHVEnabled != bool(vm_obj.config.nestedHVEnabled):
+            if vm_obj is None or nested_virt != bool(vm_obj.config.nestedHVEnabled):
                 self.change_detected = True
+                self.configspec.nestedHVEnabled = nested_virt
 
         temp_version = self.params['hardware']['version']
         if temp_version is not None:
+            new_version = None
             if temp_version.lower() == 'latest':
                 # Check is to make sure vm_obj is not of type template
                 if vm_obj and not vm_obj.config.template:
-                    try:
-                        task = vm_obj.UpgradeVM_Task()
-                        self.wait_for_task(task)
-                        if task.info.state == 'error':
-                            return {'changed': self.change_applied, 'failed': True, 'msg': task.info.error.msg, 'op': 'upgrade'}
-                    except vim.fault.AlreadyUpgraded:
-                        # Don't fail if VM is already upgraded.
-                        pass
+                    config_option_descriptors = vm_obj.environmentBrowser.QueryConfigOptionDescriptor()
+                    available_hw_versions = [int(option_desc.key.split("-")[1]) for option_desc in config_option_descriptors if option_desc.upgradeSupported]
+                    temp_version = max(available_hw_versions)
             else:
                 try:
                     temp_version = int(temp_version)
@@ -1650,55 +1654,59 @@ def configure_hardware_params(self, vm_obj):
                                           " values are either 'latest' or a number."
                                           " Please check VMware documentation for valid VM hardware versions." % temp_version)
 
+            if isinstance(temp_version, int):
+                # Hardware version is denoted as "vmx-10"
+                new_version = "vmx-%02d" % temp_version
+
+            if vm_obj is None:
+                self.change_detected = True
+                self.configspec.version = new_version
+            # Check is to make sure vm_obj is not of type template
+            elif not vm_obj.config.template:
+                # VM exists and we need to update the hardware version
+                current_version = vm_obj.config.version
                 # Hardware version is denoted as "vmx-10"
-                version = "vmx-%02d" % temp_version
-                self.configspec.version = version
-                if vm_obj is None or self.configspec.version != vm_obj.config.version:
+                version_digit = int(current_version.split("-", 1)[-1])
+                if temp_version < version_digit:
+                    self.module.fail_json(msg="Current hardware version '%d' which is greater than the specified"
+                                          " version '%d'. Downgrading hardware version is"
+                                          " not supported. Please specify version greater"
+                                          " than the current version." % (version_digit,
+                                                                          temp_version))
+                elif temp_version > version_digit:
                     self.change_detected = True
-                # Check is to make sure vm_obj is not of type template
-                if vm_obj and not vm_obj.config.template:
-                    # VM exists and we need to update the hardware version
-                    current_version = vm_obj.config.version
-                    # current_version = "vmx-10"
-                    version_digit = int(current_version.split("-", 1)[-1])
-                    if temp_version < version_digit:
-                        self.module.fail_json(msg="Current hardware version '%d' which is greater than the specified"
-                                              " version '%d'. Downgrading hardware version is"
-                                              " not supported. Please specify version greater"
-                                              " than the current version." % (version_digit,
-                                                                              temp_version))
-                    new_version = "vmx-%02d" % temp_version
-                    try:
+                    self.tracked_changes['hardware.version'] = temp_version
+                    self.configspec.version = new_version
+                    # Only perform the upgrade if not in check mode.
+                    if not self.module.check_mode:
                         task = vm_obj.UpgradeVM_Task(new_version)
                         self.wait_for_task(task)
                         if task.info.state == 'error':
                             return {'changed': self.change_applied, 'failed': True, 'msg': task.info.error.msg, 'op': 'upgrade'}
-                    except vim.fault.AlreadyUpgraded:
-                        # Don't fail if VM is already upgraded.
-                        pass
+                        self.change_applied = True
 
         secure_boot = self.params['hardware']['secure_boot']
         if secure_boot is not None:
-            self.configspec.bootOptions = vim.vm.BootOptions()
-            self.configspec.bootOptions.efiSecureBootEnabled = True
-            if vm_obj is None or self.configspec.bootOptions.efiSecureBootEnabled != vm_obj.config.bootOptions.efiSecureBootEnabled:
+            if vm_obj is None or secure_boot != vm_obj.config.bootOptions.efiSecureBootEnabled:
                 self.change_detected = True
+                self.configspec.bootOptions = vim.vm.BootOptions()
+                self.configspec.bootOptions.efiSecureBootEnabled = secure_boot
 
         iommu = self.params['hardware']['iommu']
         if iommu is not None:
-            if self.configspec.flags is None:
-                self.configspec.flags = vim.vm.FlagInfo()
-            self.configspec.flags.vvtdEnabled = iommu
-            if vm_obj is None or self.configspec.flags.vvtdEnabled != vm_obj.config.flags.vvtdEnabled:
+            if vm_obj is None or iommu != vm_obj.config.flags.vvtdEnabled:
                 self.change_detected = True
+                if self.configspec.flags is None:
+                    self.configspec.flags = vim.vm.FlagInfo()
+                self.configspec.flags.vvtdEnabled = iommu
 
         virt_based_security = self.params['hardware']['virt_based_security']
         if virt_based_security is not None:
-            if self.configspec.flags is None:
-                self.configspec.flags = vim.vm.FlagInfo()
-            self.configspec.flags.vbsEnabled = virt_based_security
-            if vm_obj is None or vm_obj.config.flags.vbsEnabled != self.configspec.flags.vbsEnabled:
+            if vm_obj is None or virt_based_security != self.configspec.flags.vbsEnabled:
                 self.change_detected = True
+                if self.configspec.flags is None:
+                    self.configspec.flags = vim.vm.FlagInfo()
+                self.configspec.flags.vbsEnabled = virt_based_security
 
     def get_device_by_type(self, vm=None, type=None):
         device_list = []
@@ -1738,7 +1746,8 @@ def configure_nvdimm(self, vm_obj):
                                           " when state is set to 'absent'.")
             # Reconfigure device requires VM in power off state
             if vm_obj and not vm_obj.config.template:
-                if vm_obj.runtime.powerState != vim.VirtualMachinePowerState.poweredOff:
+                # Allow VM to be powered on during this check when in check mode, when no changes will actually be made
+                if vm_obj.runtime.powerState != vim.VirtualMachinePowerState.poweredOff and not self.module.check_mode:
                     self.module.fail_json(msg="VM is not in power off state, can not do virtual NVDIMM configuration.")
 
             nvdimm_ctl_exists = False
@@ -2553,8 +2562,10 @@ def configure_multiple_controllers_disks(self, vm_obj):
                 disk_ctl_spec.device = disk_ctl
             for j in range(0, len(ctl['disk'])):
                 hard_disk = None
+                hard_disk_spec = None
                 hard_disk_exist = False
-                disk_modified = False
+                disk_modified_for_spec = False
+                disk_modified_for_disk = False
                 disk_unit_number = ctl['disk'][j]['unit_number']
                 # from attached disk list find the specified one
                 if len(disk_list) != 0:
@@ -2568,16 +2579,20 @@ def configure_multiple_controllers_disks(self, vm_obj):
                     hard_disk_spec = vim.vm.device.VirtualDeviceSpec()
                     hard_disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
                     hard_disk_spec.device = hard_disk
-                    disk_modified = self.set_disk_parameters(hard_disk_spec, ctl['disk'][j], reconfigure=True)
-                    self.configspec.deviceChange.append(hard_disk_spec)
+                    disk_modified_for_spec = self.set_disk_parameters(hard_disk_spec, ctl['disk'][j], reconfigure=True)
                 # if no disk or the specified one not exist do create new disk
                 if len(disk_list) == 0 or not hard_disk_exist:
                     hard_disk = self.device_helper.create_hard_disk(disk_ctl_spec, disk_unit_number)
                     hard_disk.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.create
-                    disk_modified = self.set_disk_parameters(hard_disk, ctl['disk'][j])
-                    self.configspec.deviceChange.append(hard_disk)
-                if disk_modified:
+                    disk_modified_for_disk = self.set_disk_parameters(hard_disk, ctl['disk'][j])
+
+                # Only update the configspec that will be applied in reconfigure_vm if something actually changed
+                if disk_modified_for_spec:
+                    self.change_detected = True
+                    self.configspec.deviceChange.append(hard_disk_spec)
+                if disk_modified_for_disk:
                     self.change_detected = True
+                    self.configspec.deviceChange.append(hard_disk)
 
     def configure_disks(self, vm_obj):
         # Ignore empty disk list, this permits to keep disks when deploying a template/cloning a VM
@@ -3159,35 +3174,49 @@ def reconfigure_vm(self):
             self.relospec.pool = self.get_resource_pool()
 
             if self.relospec.pool != self.current_vm_obj.resourcePool:
-                task = self.current_vm_obj.RelocateVM_Task(spec=self.relospec)
-                self.wait_for_task(task)
-                if task.info.state == 'error':
-                    return {'changed': self.change_applied, 'failed': True, 'msg': task.info.error.msg, 'op': 'relocate'}
+                self.tracked_changes['resourcePool'] = str(self.relospec.pool)
+                if self.module.check_mode:
+                    self.change_applied = True
+                else:
+                    task = self.current_vm_obj.RelocateVM_Task(spec=self.relospec)
+                    self.wait_for_task(task)
+                    if task.info.state == 'error':
+                        return {'changed': self.change_applied, 'failed': True, 'msg': task.info.error.msg, 'op': 'relocate'}
 
         # Only send VMware task if we see a modification
         if self.change_detected:
-            task = None
-            try:
-                task = self.current_vm_obj.ReconfigVM_Task(spec=self.configspec)
-            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))
-            self.wait_for_task(task)
-            if task.info.state == 'error':
-                return {'changed': self.change_applied, 'failed': True, 'msg': task.info.error.msg, 'op': 'reconfig'}
+            self.tracked_changes['configspec'] = str(self.configspec)
+            if self.module.check_mode:
+                self.change_applied = True
+            else:
+                task = None
+                try:
+                    task = self.current_vm_obj.ReconfigVM_Task(spec=self.configspec)
+                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))
+                self.wait_for_task(task)
+                if task.info.state == 'error':
+                    return {'changed': self.change_applied, 'failed': True, 'msg': task.info.error.msg, 'op': 'reconfig'}
 
         # Rename VM
         if self.params['uuid'] and self.params['name'] and self.params['name'] != self.current_vm_obj.config.name:
-            task = self.current_vm_obj.Rename_Task(self.params['name'])
-            self.wait_for_task(task)
-            if task.info.state == 'error':
-                return {'changed': self.change_applied, 'failed': True, 'msg': task.info.error.msg, 'op': 'rename'}
+            self.tracked_changes['name'] = self.params['name']
+            if self.module.check_mode:
+                self.change_applied = True
+            else:
+                task = self.current_vm_obj.Rename_Task(self.params['name'])
+                self.wait_for_task(task)
+                if task.info.state == 'error':
+                    return {'changed': self.change_applied, 'failed': True, 'msg': task.info.error.msg, 'op': 'rename'}
 
         # Mark VM as Template
         if self.params['is_template'] and not self.current_vm_obj.config.template:
             try:
-                self.current_vm_obj.MarkAsTemplate()
+                if not self.module.check_mode:
+                    self.current_vm_obj.MarkAsTemplate()
                 self.change_applied = True
+                self.tracked_changes['MarkAsTemplate'] = True
             except vmodl.fault.NotSupported as e:
                 self.module.fail_json(msg="Failed to mark virtual machine [%s] "
                                           "as template: %s" % (self.params['name'], e.msg))
@@ -3202,8 +3231,10 @@ def reconfigure_vm(self):
                 kwargs.update(host=host_system_obj)
 
             try:
-                self.current_vm_obj.MarkAsVirtualMachine(**kwargs)
+                if not self.module.check_mode:
+                    self.current_vm_obj.MarkAsVirtualMachine(**kwargs)
                 self.change_applied = True
+                self.tracked_changes['MarkAsVirtualMachine'] = True
             except vim.fault.InvalidState as invalid_state:
                 self.module.fail_json(msg="Virtual machine is not marked"
                                           " as template : %s" % to_native(invalid_state.msg))
@@ -3222,29 +3253,25 @@ def reconfigure_vm(self):
                 self.module.fail_json(msg="Failed to convert template to virtual machine"
                                           " due to generic error : %s" % to_native(generic_exc))
 
-            # Automatically update VMware UUID when converting template to VM.
-            # This avoids an interactive prompt during VM startup.
-            uuid_action = [x for x in self.current_vm_obj.config.extraConfig if x.key == "uuid.action"]
-            if not uuid_action:
-                uuid_action_opt = vim.option.OptionValue()
-                uuid_action_opt.key = "uuid.action"
-                uuid_action_opt.value = "create"
-                self.configspec.extraConfig.append(uuid_action_opt)
-
-            self.change_detected = True
-
         # add customize existing VM after VM re-configure
         if self.params['customization']['existing_vm']:
             if self.current_vm_obj.config.template:
                 self.module.fail_json(msg="VM is template, not support guest OS customization.")
-            if self.current_vm_obj.runtime.powerState != vim.VirtualMachinePowerState.poweredOff:
+            if self.current_vm_obj.runtime.powerState != vim.VirtualMachinePowerState.poweredOff and not self.module.check_mode:
                 self.module.fail_json(msg="VM is not in poweroff state, can not do guest OS customization.")
-            cus_result = self.customize_exist_vm()
-            if cus_result['failed']:
-                return cus_result
+            # TODO not sure if it is possible to query the current customspec to compare against the one being provided to check in check mode.
+            # Maybe by breaking down the individual fields and querying, but it needs more research.
+            # For now, assume changed...
+            self.tracked_changes['customization'] = True
+            if self.module.check_mode:
+                self.change_applied = True
+            else:
+                cus_result = self.customize_exist_vm()
+                if cus_result['failed']:
+                    return cus_result
 
         vm_facts = self.gather_facts(self.current_vm_obj)
-        return {'changed': self.change_applied, 'failed': False, 'instance': vm_facts}
+        return {'changed': self.change_applied, 'failed': False, 'instance': vm_facts, 'changes': self.tracked_changes}
 
     def customize_exist_vm(self):
         task = None
@@ -3501,23 +3528,26 @@ def main():
                 set_vm_power_state(pyv.content, vm, 'poweredoff', module.params['force'])
             result = pyv.remove_vm(vm, module.params['delete_from_inventory'])
         elif module.params['state'] == 'present':
-            if module.check_mode:
-                result.update(
-                    vm_name=vm.name,
-                    changed=True,
-                    desired_operation='reconfigure_vm',
-                )
-                module.exit_json(**result)
+            # Note that check_mode is handled inside reconfigure_vm
             result = pyv.reconfigure_vm()
         elif module.params['state'] in ['poweredon', 'powered-on', 'poweredoff',
                                         'powered-off', 'restarted', 'suspended',
                                         'shutdownguest', 'shutdown-guest',
                                         'rebootguest', 'reboot-guest']:
             if module.check_mode:
+                # Identify if the power state would have changed if not in check mode
+                current_powerstate = vm.summary.runtime.powerState.lower()
+                powerstate_will_change = False
+                if ((current_powerstate == 'poweredon' and module.params['state'] not in ['poweredon', 'powered-on'])
+                        or (current_powerstate == 'poweredoff' and module.params['state']
+                            not in ['poweredoff', 'powered-off', 'shutdownguest', 'shutdown-guest'])
+                        or (current_powerstate == 'suspended' and module.params['state'] != 'suspended')):
+                    powerstate_will_change = True
+
                 result.update(
                     vm_name=vm.name,
-                    changed=True,
-                    current_powerstate=vm.summary.runtime.powerState.lower(),
+                    changed=powerstate_will_change,
+                    current_powerstate=current_powerstate,
                     desired_operation='set_vm_power_state',
                 )
                 module.exit_json(**result)


Test patch:
diff --git a/tests/integration/targets/vmware_guest/tasks/advanced_settings.yml b/tests/integration/targets/vmware_guest/tasks/advanced_settings.yml
--- a/tests/integration/targets/vmware_guest/tasks/advanced_settings.yml
+++ b/tests/integration/targets/vmware_guest/tasks/advanced_settings.yml
@@ -29,6 +29,29 @@
     that:
       - r is changed
 
+- name: Change advance settings for VM in check mode
+  vmware_guest:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    name: test_vm1
+    datacenter: "{{ dc1 }}"
+    state: present
+    folder: "{{ f0 }}"
+    advanced_settings:
+      - key: "svga.present"
+        value: "FALSE"
+  register: r
+  check_mode: true
+
+- debug: var=r
+
+- name: Assert that changes would be made
+  assert:
+    that:
+      - r is changed
+
 - name: Change advance settings for VM
   vmware_guest:
     validate_certs: false
diff --git a/tests/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f0.yml b/tests/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f0.yml
--- a/tests/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f0.yml
+++ b/tests/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f0.yml
@@ -31,6 +31,30 @@
     that:
       - "cdrom_vm.changed == true"
 
+- name: Update CDROM to iso for the new VM in check mode
+  vmware_guest:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    folder: "vm"
+    name: test_vm1
+    datastore: "{{ rw_datastore }}"
+    datacenter: "{{ dc1 }}"
+    cdrom:
+      type: iso
+      iso_path: "[{{ ro_datastore }}] fedora.iso"
+    state: present
+  register: cdrom_vm
+  check_mode: true
+
+- debug: var=cdrom_vm
+
+- name: assert the VM would be changed
+  assert:
+    that:
+      - "cdrom_vm.changed == true"
+
 - name: Update CDROM to iso for the new VM
   vmware_guest:
     validate_certs: false
@@ -54,6 +78,28 @@
     that:
       - "cdrom_vm.changed == true"
 
+- name: Update CDROM to client for the new VM in check mode
+  vmware_guest:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    folder: vm
+    name: test_vm1
+    datacenter: "{{ dc1 }}"
+    cdrom:
+      type: client
+    state: present
+  register: cdrom_vm
+  check_mode: true
+
+- debug: var=cdrom_vm
+
+- name: assert the VM would be changed
+  assert:
+    that:
+      - "cdrom_vm.changed == true"
+
 - name: Update CDROM to client for the new VM
   vmware_guest:
     validate_certs: false
diff --git a/tests/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f1.yml b/tests/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f1.yml
--- a/tests/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f1.yml
+++ b/tests/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f1.yml
@@ -45,6 +45,44 @@
     that:
       - "cdrom_vm.changed == true"
 
+- name: Remove the last 2 CDROMs and update the first 2 for the new VM in check mode
+  vmware_guest:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    folder: vm
+    name: test_vm1
+    datacenter: "{{ dc1 }}"
+    cdrom:
+    - controller_type: ide
+      controller_number: 0
+      unit_number: 0
+      type: client
+    - controller_type: ide
+      controller_number: 0
+      unit_number: 1
+      type: iso
+      iso_path: "[{{ ro_datastore }}] fedora.iso"
+    - controller_type: ide
+      controller_number: 1
+      unit_number: 0
+      state: absent
+    - controller_type: ide
+      controller_number: 1
+      unit_number: 1
+      state: absent
+    state: present
+  register: cdrom_vm
+  check_mode: true
+
+- debug: var=cdrom_vm
+
+- name: assert the VM would be changed
+  assert:
+    that:
+      - "cdrom_vm.changed == true"
+
 - name: Remove the last 2 CDROMs and update the first 2 for the new VM
   vmware_guest:
     validate_certs: false
@@ -129,6 +167,44 @@
     that:
       - "cdrom_vm.changed == true"
 
+- name: Remove the first 2 CDROMs and update the last 2 for the new VM in check mode
+  vmware_guest:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    folder: vm
+    name: test_vm2
+    datacenter: "{{ dc1 }}"
+    cdrom:
+    - controller_type: sata
+      controller_number: 0
+      unit_number: 0
+      state: absent
+    - controller_type: sata
+      controller_number: 1
+      unit_number: 1
+      state: absent
+    - controller_type: sata
+      controller_number: 2
+      unit_number: 0
+      type: client
+    - controller_type: sata
+      controller_number: 3
+      unit_number: 1
+      type: iso
+      iso_path: "[{{ ro_datastore }}] fedora.iso"
+    state: present
+  register: cdrom_vm
+  check_mode: true
+
+- debug: var=cdrom_vm
+
+- name: assert the VM would be changed
+  assert:
+    that:
+      - "cdrom_vm.changed == true"
+
 - name: Remove the first 2 CDROMs and update the last 2 for the new VM
   vmware_guest:
     validate_certs: false
@@ -213,6 +289,44 @@
     that:
       - "cdrom_vm.changed == true"
 
+- name: Remove the 2 CDROMs and update the other 2 for the new VM in check mode
+  vmware_guest:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    folder: vm
+    name: test_vm3
+    datacenter: "{{ dc1 }}"
+    cdrom:
+    - controller_type: ide
+      controller_number: 0
+      unit_number: 0
+      state: absent
+    - controller_type: sata
+      controller_number: 0
+      unit_number: 0
+      type: iso
+      iso_path: "[{{ ro_datastore }}] fedora.iso"
+    - controller_type: ide
+      controller_number: 1
+      unit_number: 0
+      type: client
+    - controller_type: sata
+      controller_number: 1
+      unit_number: 0
+      state: absent
+    state: present
+  register: cdrom_vm
+  check_mode: true
+
+- debug: var=cdrom_vm
+
+- name: assert the VM would be changed
+  assert:
+    that:
+      - "cdrom_vm.changed == true"
+
 - name: Remove the 2 CDROMs and update the other 2 for the new VM
   vmware_guest:
     validate_certs: false
diff --git a/tests/integration/targets/vmware_guest/tasks/check_mode.yml b/tests/integration/targets/vmware_guest/tasks/check_mode.yml
--- a/tests/integration/targets/vmware_guest/tasks/check_mode.yml
+++ b/tests/integration/targets/vmware_guest/tasks/check_mode.yml
@@ -49,11 +49,11 @@
   register: check_mode_state
   check_mode: true
 
-- name: assert that changes were made
+- name: assert that changes were appropriate for an existing powered-off VM
   assert:
     that:
-      - "check_mode_state.results|map(attribute='changed')|unique|list == [true]"
-      - "check_mode_state.results|map(attribute='vm_name')|unique|list == [ virtual_machines[0].name ]"
+      - "check_mode_state.results|map(attribute='changed') == [true, false, false, false, true, true, true, true, false, false, true, true]"
+      #- "check_mode_state.results|map(attribute='vm_name')|unique|list == [ virtual_machines[0].name ]"
 
 - name: Perform all operation on non-existent VM in check mode
   vmware_guest:
diff --git a/tests/integration/targets/vmware_guest/tasks/reconfig_vm_to_latest_version.yml b/tests/integration/targets/vmware_guest/tasks/reconfig_vm_to_latest_version.yml
--- a/tests/integration/targets/vmware_guest/tasks/reconfig_vm_to_latest_version.yml
+++ b/tests/integration/targets/vmware_guest/tasks/reconfig_vm_to_latest_version.yml
@@ -49,6 +49,30 @@
     that:
         - deploy_vm_to_latest is changed
 
+- name: Upgrade VM to latest version in check mode
+  vmware_guest:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    name: test_vm1
+    guest_id: debian8_64Guest
+    datacenter: "{{ dc1 }}"
+    folder: "{{ f0 }}"
+    datastore: '{{ rw_datastore }}'
+    hardware:
+        num_cpus: 1
+        memory_mb: 1028
+        version: latest
+    state: present
+  register: upgrade_vm
+  check_mode: true
+
+- name: assert that changes would be made
+  assert:
+    that:
+        - upgrade_vm is changed
+
 - name: Upgrade VM to latest version
   vmware_guest:
     validate_certs: false
diff --git a/tests/integration/targets/vmware_guest/tasks/windows_vbs_d1_c1_f0.yml b/tests/integration/targets/vmware_guest/tasks/windows_vbs_d1_c1_f0.yml
--- a/tests/integration/targets/vmware_guest/tasks/windows_vbs_d1_c1_f0.yml
+++ b/tests/integration/targets/vmware_guest/tasks/windows_vbs_d1_c1_f0.yml
@@ -67,6 +67,32 @@
       - "vbs_vm.failed == false"
       - "vbs_vm.changed == true"
 
+- name: Enable VBS for Windows Server 2016 VM in check mode
+  vmware_guest:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    folder: "{{ f0 }}"
+    name: vbs-Test2
+    datacenter: "{{ f0 }}"
+    disk:
+    - size_mb: 256
+      type: thin
+      datastore: '{{ rw_datastore }}'
+    hardware:
+      virt_based_security: true
+    state: present
+  register: vbs_vm
+  check_mode: true
+
+- debug: var=vbs_vm
+
+- name: assert the VM would be changed
+  assert:
+    that:
+      - vbs_vm is changed
+
 - name: Enable VBS for Windows Server 2016 VM
   vmware_guest:
     validate_certs: false