Repository: ansible-collections/community.vmware

Issue:
[New Feature] Add support for virtual NVDIMM device of VM
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->
PMem was introduced on ESXi 6.7, refer to this page for more info: https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.resmgmt.doc/GUID-EB72D358-9C2C-4FBD-81A9-A145E155CE31.html

So request to add the feature support in community.vmware collection, add new vPMem disk, add new NVDIMM device for VM.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
vmware_guest_disk?

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- HINT: You can also paste gist.github.com links for larger files -->



Discussion:
Files identified in the description:
* [`plugins/modules/vmware_guest_disk.py`](https://github.com/['ansible-collections/amazon.aws', 'ansible-collections/community.aws', 'ansible-collections/community.vmware']/blob/main/plugins/modules/vmware_guest_disk.py)

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 --->
cc @Akasurde @goneri @lparkes @pgbidkar @warthog9
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Now that #909 and #914 are merged, I'd like to work on making vmware_guest_disk use module_utils.vm_device_helper to create disks. Then we could implement vPMem disk / NVDIMM devices there, thus making it available to both vmware_guest_disk and vmware_guest.

Btw: Do you want to implement this because you have a real use case for this, or just because it's possible? When I hear [NVDIMM](https://en.wikipedia.org/wiki/NVDIMM), I think [3D XPoint](https://en.wikipedia.org/wiki/3D_XPoint). And this looks pretty dead to me. Micron doesn't work on this any more, and personally I think Intel keeps producing Optane because they just don't want to officially acknowledge they've invested a lot of money into a product nobody wants.
I don't have data about how many customers are using vPMem disk or NVDIMM device of VM on vSphere in real world, and there are some limitations for VM with NVDIMM device or vPMem disk, might be one of the reasons users not use it.

This request comes from the project we started using Ansible to automate the GOS certification testing [here](https://github.com/vmware/ansible-vsphere-gos-validation). Some functionalities or VM virutal device types on vSphere not implemented in this collection and we need them in our test suite.
@Tomorrow9 I'd like to move some more code from `vmware_guest_disk` to `vm_device_helper` first.

However, what would we need to implement for vPMem disks / NVDIMM devices? I guess we need a function in `vm_device_helper` to create a [VirtualNVDIMMController](https://vdc-download.vmware.com/vmwb-repository/dcr-public/8946c1b6-2861-4c12-a45f-f14ae0d3b1b9/a5b8094c-c222-4307-9399-3b606a04af55/vim.vm.device.VirtualNVDIMMController.html), right? And for the PMem disks / NVDIMM devices themselves we would use [VirtualDiskLocalPMemBackingInfo](https://vdc-download.vmware.com/vmwb-repository/dcr-public/8946c1b6-2861-4c12-a45f-f14ae0d3b1b9/a5b8094c-c222-4307-9399-3b606a04af55/vim.vm.device.VirtualDisk.LocalPMemBackingInfo.html) / [VirtualNVDIMMBackingInfo](https://vdc-download.vmware.com/vmwb-repository/dcr-public/8946c1b6-2861-4c12-a45f-f14ae0d3b1b9/a5b8094c-c222-4307-9399-3b606a04af55/vim.vm.device.VirtualNVDIMM.BackingInfo.html) as `backing`?
> However, what would we need to implement for vPMem disks / NVDIMM devices? I guess we need a function in `vm_device_helper` to create a [VirtualNVDIMMController](https://vdc-download.vmware.com/vmwb-repository/dcr-public/8946c1b6-2861-4c12-a45f-f14ae0d3b1b9/a5b8094c-c222-4307-9399-3b606a04af55/vim.vm.device.VirtualNVDIMMController.html), right? And for the PMem disks / NVDIMM devices themselves we would use [VirtualDiskLocalPMemBackingInfo](https://vdc-download.vmware.com/vmwb-repository/dcr-public/8946c1b6-2861-4c12-a45f-f14ae0d3b1b9/a5b8094c-c222-4307-9399-3b606a04af55/vim.vm.device.VirtualDisk.LocalPMemBackingInfo.html) / [VirtualNVDIMMBackingInfo](https://vdc-download.vmware.com/vmwb-repository/dcr-public/8946c1b6-2861-4c12-a45f-f14ae0d3b1b9/a5b8094c-c222-4307-9399-3b606a04af55/vim.vm.device.VirtualNVDIMM.BackingInfo.html) as `backing`?

Yeah, you are right, we need to add creating these 2 "VirtualNVDIMMController", "VirtualNVDIMM" new device types, and a new "VirtualDisk" with "VirtualDiskLocalPMemBackingInfo".





@Tomorrow9 

I'm sorry, but I don't understand how to implement vPMem disks / NVDIMM devices. If you do, feel free to give it a try. But please wait until #926 is merged so we don't run into any merge conflicts.

Btw: It would be great if you implemented the things that could be useful for several modules (like `vmware_guest` and `vmware_guest_disk`) in `module_utils/vm_device_helper.py`.
@mariolenz sure, I'm working on adding/removing new NVDIMM device in `vmware_guest` module firstly. After that, will consider adding vPmem disk support in `vmware_guest` module and `vmware_guest_disk` module. But common virtual device operations will be added in `module_utils/vm_device_helper.py`. Thanks.

Resolving patch:
diff --git a/plugins/module_utils/vm_device_helper.py b/plugins/module_utils/vm_device_helper.py
--- a/plugins/module_utils/vm_device_helper.py
+++ b/plugins/module_utils/vm_device_helper.py
@@ -313,3 +313,60 @@ def integer_value(self, input_value, name):
         else:
             self.module.fail_json(msg='"%s" attribute should be an'
                                   ' integer value.' % name)
+
+    def create_nvdimm_controller(self):
+        nvdimm_ctl = vim.vm.device.VirtualDeviceSpec()
+        nvdimm_ctl.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
+        nvdimm_ctl.device = vim.vm.device.VirtualNVDIMMController()
+        nvdimm_ctl.device.deviceInfo = vim.Description()
+        nvdimm_ctl.device.key = -randint(27000, 27999)
+
+        return nvdimm_ctl
+
+    @staticmethod
+    def is_nvdimm_controller(device):
+        return isinstance(device, vim.vm.device.VirtualNVDIMMController)
+
+    def create_nvdimm_device(self, nvdimm_ctl_dev_key, pmem_profile_id, nvdimm_dev_size_mb=1024):
+        nvdimm_dev_spec = vim.vm.device.VirtualDeviceSpec()
+        nvdimm_dev_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
+        nvdimm_dev_spec.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.create
+        nvdimm_dev_spec.device = vim.vm.device.VirtualNVDIMM()
+        nvdimm_dev_spec.device.controllerKey = nvdimm_ctl_dev_key
+        nvdimm_dev_spec.device.key = -randint(28000, 28999)
+        nvdimm_dev_spec.device.capacityInMB = nvdimm_dev_size_mb
+        nvdimm_dev_spec.device.deviceInfo = vim.Description()
+        nvdimm_dev_spec.device.backing = vim.vm.device.VirtualNVDIMM.BackingInfo()
+        profile = vim.vm.DefinedProfileSpec()
+        profile.profileId = pmem_profile_id
+        nvdimm_dev_spec.profile = [profile]
+
+        return nvdimm_dev_spec
+
+    @staticmethod
+    def is_nvdimm_device(device):
+        return isinstance(device, vim.vm.device.VirtualNVDIMM)
+
+    def find_nvdimm_by_label(self, nvdimm_label, nvdimm_devices):
+        nvdimm_dev = None
+        for nvdimm in nvdimm_devices:
+            if nvdimm.deviceInfo.label == nvdimm_label:
+                nvdimm_dev = nvdimm
+
+        return nvdimm_dev
+
+    def remove_nvdimm(self, nvdimm_device):
+        nvdimm_spec = vim.vm.device.VirtualDeviceSpec()
+        nvdimm_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.remove
+        nvdimm_spec.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.destroy
+        nvdimm_spec.device = nvdimm_device
+
+        return nvdimm_spec
+
+    def update_nvdimm_config(self, nvdimm_device, nvdimm_size):
+        nvdimm_spec = vim.vm.device.VirtualDeviceSpec()
+        nvdimm_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
+        nvdimm_spec.device = nvdimm_device
+        nvdimm_device.capacityInMB = nvdimm_size
+
+        return nvdimm_spec
diff --git a/plugins/module_utils/vmware_spbm.py b/plugins/module_utils/vmware_spbm.py
--- a/plugins/module_utils/vmware_spbm.py
+++ b/plugins/module_utils/vmware_spbm.py
@@ -41,3 +41,19 @@ def get_spbm_connection(self):
 
         self.spbm_si = pbm.ServiceInstance("ServiceInstance", stub)
         self.spbm_content = self.spbm_si.PbmRetrieveServiceContent()
+
+    def find_storage_profile_by_name(self, profile_name):
+        storage_profile = None
+        self.get_spbm_connection()
+        pm = self.spbm_content.profileManager
+        profile_ids = pm.PbmQueryProfile(resourceType=pbm.profile.ResourceType(resourceType="STORAGE"),
+                                         profileCategory="REQUIREMENT")
+        if len(profile_ids) > 0:
+            storage_profiles = pm.PbmRetrieveContent(profileIds=profile_ids)
+            for profile in storage_profiles:
+                if profile.name == profile_name:
+                    storage_profile = profile
+        else:
+            self.module.warn("Unable to get storage profile IDs with STORAGE resource type and REQUIREMENT profile category.")
+
+        return storage_profile
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
@@ -314,6 +314,33 @@
             - C(controller_type), C(controller_number) and C(unit_number) are required when creating or reconfiguring VMs
               with multiple types of disk controllers and disks.
             - When creating new VM, the first configured disk in the C(disk) list will be "Hard Disk 1".
+  nvdimm:
+    description:
+    - Add or remove a virtual NVDIMM device to the virtual machine.
+    - VM virtual hardware version must be 14 or higher on vSphere 6.7 or later.
+    - Verify that guest OS of the virtual machine supports PMem before adding virtual NVDIMM device.
+    - Verify that you have the I(Datastore.Allocate) space privilege on the virtual machine.
+    - Make sure that the host or the cluster on which the virtual machine resides has available PMem resources.
+    - To add or remove virtual NVDIMM device to the existing virtual machine, it must be in power off state.
+    type: dict
+    version_added: '1.13.0'
+    suboptions:
+        state:
+             type: str
+             description:
+             - Valid value is C(present) or C(absent).
+             - If set to C(absent), then the NVDIMM device with specified C(label) will be removed.
+             choices: ['present', 'absent']
+        size_mb:
+            type: int
+            description: Virtual NVDIMM device size in MB.
+            default: 1024
+        label:
+             type: str
+             description:
+             - The label of the virtual NVDIMM device to be removed or configured, e.g., "NVDIMM 1".
+             - 'This parameter is required when C(state) is set to C(absent), or C(present) to reconfigure NVDIMM device
+               size. When add a new device, please do not set C(label).'
   cdrom:
     description:
     - A list of CD-ROM configurations for the virtual machine. Added in version 2.9.
@@ -967,6 +994,29 @@
       device_type: vmxnet3
   delegate_to: localhost
   register: deploy_vm
+
+- name: Create a VM with NVDIMM device
+  community.vmware.vmware_guest:
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    folder: /DC1/vm/
+    name: test_vm_nvdimm
+    state: poweredoff
+    guest_id: centos7_64Guest
+    datastore: datastore1
+    hardware:
+      memory_mb: 512
+      num_cpus: 4
+      version: 14
+    networks:
+    - name: VM Network
+      device_type: vmxnet3
+    nvdimm:
+      state: present
+      size_mb: 2048
+  delegate_to: localhost
+  register: deploy_vm
 '''
 
 RETURN = r'''
@@ -1006,6 +1056,7 @@
     quote_obj_name,
 )
 from ansible_collections.community.vmware.plugins.module_utils.vm_device_helper import PyVmomiDeviceHelper
+from ansible_collections.community.vmware.plugins.module_utils.vmware_spbm import SPBM
 
 
 class PyVmomiCache(object):
@@ -1571,6 +1622,84 @@ def get_vm_ide_devices(self, vm=None):
     def get_vm_sata_devices(self, vm=None):
         return self.get_device_by_type(vm=vm, type=vim.vm.device.VirtualAHCIController)
 
+    def get_vm_nvdimm_ctl_device(self, vm=None):
+        return self.get_device_by_type(vm=vm, type=vim.vm.device.VirtualNVDIMMController)
+
+    def get_vm_nvdimm_devices(self, vm=None):
+        return self.get_device_by_type(vm=vm, type=vim.vm.device.VirtualNVDIMM)
+
+    def configure_nvdimm(self, vm_obj):
+        """
+        Manage virtual NVDIMM device to the virtual machine
+        Args:
+            vm_obj: virtual machine object
+        """
+        if self.params['nvdimm']['state']:
+            # Label is required when remove device
+            if self.params['nvdimm']['state'] == 'absent' and not self.params['nvdimm']['label']:
+                self.module.fail_json(msg="Please specify the label of virtual NVDIMM device using 'label' parameter"
+                                          " 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:
+                    self.module.fail_json(msg="VM is not in power off state, can not do virtual NVDIMM configuration.")
+
+            nvdimm_ctl_exists = False
+            if vm_obj and not vm_obj.config.template:
+                # Get existing NVDIMM controller
+                nvdimm_ctl = self.get_vm_nvdimm_ctl_device(vm=vm_obj)
+                if len(nvdimm_ctl) != 0:
+                    nvdimm_ctl_exists = True
+                    nvdimm_ctl_key = nvdimm_ctl[0].key
+                    if self.params['nvdimm']['label'] is not None:
+                        nvdimm_devices = self.get_vm_nvdimm_devices(vm=vm_obj)
+                        if len(nvdimm_devices) != 0:
+                            existing_nvdimm_dev = self.device_helper.find_nvdimm_by_label(
+                                nvdimm_label=self.params['nvdimm']['label'],
+                                nvdimm_devices=nvdimm_devices
+                            )
+                            if existing_nvdimm_dev is not None:
+                                if self.params['nvdimm']['state'] == 'absent':
+                                    nvdimm_remove_spec = self.device_helper.remove_nvdimm(
+                                        nvdimm_device=existing_nvdimm_dev
+                                    )
+                                    self.change_detected = True
+                                    self.configspec.deviceChange.append(nvdimm_remove_spec)
+                                else:
+                                    if existing_nvdimm_dev.capacityInMB < self.params['nvdimm']['size_mb']:
+                                        nvdimm_config_spec = self.device_helper.update_nvdimm_config(
+                                            nvdimm_device=existing_nvdimm_dev,
+                                            nvdimm_size=self.params['nvdimm']['size_mb']
+                                        )
+                                        self.change_detected = True
+                                        self.configspec.deviceChange.append(nvdimm_config_spec)
+                                    elif existing_nvdimm_dev.capacityInMB > self.params['nvdimm']['size_mb']:
+                                        self.module.fail_json(msg="Can not change NVDIMM device size to %s MB, which is"
+                                                                  " smaller than the current size %s MB."
+                                                                  % (self.params['nvdimm']['size_mb'],
+                                                                     existing_nvdimm_dev.capacityInMB))
+            # New VM or existing VM without label specified, add new NVDIMM device
+            if vm_obj is None or (vm_obj and not vm_obj.config.template and self.params['nvdimm']['label'] is None):
+                if self.params['nvdimm']['state'] == 'present':
+                    # Get host default PMem storage policy
+                    storage_profile_name = "Host-local PMem Default Storage Policy"
+                    spbm = SPBM(self.module)
+                    pmem_profile = spbm.find_storage_profile_by_name(profile_name=storage_profile_name)
+                    if pmem_profile is None:
+                        self.module.fail_json(msg="Can not find PMem storage policy with name '%s'." % storage_profile_name)
+                    if not nvdimm_ctl_exists:
+                        nvdimm_ctl_spec = self.device_helper.create_nvdimm_controller()
+                        self.configspec.deviceChange.append(nvdimm_ctl_spec)
+                        nvdimm_ctl_key = nvdimm_ctl_spec.device.key
+
+                    nvdimm_dev_spec = self.device_helper.create_nvdimm_device(
+                        nvdimm_ctl_dev_key=nvdimm_ctl_key,
+                        pmem_profile_id=pmem_profile.profileId.uniqueId,
+                        nvdimm_dev_size_mb=self.params['nvdimm']['size_mb']
+                    )
+                    self.change_detected = True
+                    self.configspec.deviceChange.append(nvdimm_dev_spec)
+
     def get_vm_network_interfaces(self, vm=None):
         device_list = []
         if vm is None:
@@ -2741,6 +2870,7 @@ def deploy_vm(self):
         self.configure_disks(vm_obj=vm_obj)
         self.configure_network(vm_obj=vm_obj)
         self.configure_cdrom(vm_obj=vm_obj)
+        self.configure_nvdimm(vm_obj=vm_obj)
 
         # Find if we need network customizations (find keys in dictionary that requires customizations)
         network_changes = False
@@ -2917,6 +3047,7 @@ def reconfigure_vm(self):
         self.configure_disks(vm_obj=self.current_vm_obj)
         self.configure_network(vm_obj=self.current_vm_obj)
         self.configure_cdrom(vm_obj=self.current_vm_obj)
+        self.configure_nvdimm(vm_obj=self.current_vm_obj)
         self.customize_advanced_settings(vm_obj=self.current_vm_obj, config_spec=self.configspec)
         self.customize_customvalues(vm_obj=self.current_vm_obj)
         self.configure_resource_alloc_info(vm_obj=self.current_vm_obj)
@@ -3145,6 +3276,15 @@ def main():
                 unit_number=dict(type='int'),
             )
         ),
+        nvdimm=dict(
+            type='dict',
+            default={},
+            options=dict(
+                state=dict(type='str', choices=['present', 'absent']),
+                label=dict(type='str'),
+                size_mb=dict(type='int', default=1024),
+            )
+        ),
         cdrom=dict(type='raw', default=[]),
         hardware=dict(
             type='dict',
@@ -3222,9 +3362,7 @@ def main():
                                ['name', 'uuid'],
                            ],
                            )
-
     result = {'failed': False, 'changed': False}
-
     pyv = PyVmomiHelper(module)
 
     # Check requirements for virtualization based security


Test patch: