Repository: ansible/ansible

Issue:
vmware_guest: vm_cdrom
##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
vmware_guest

##### ANSIBLE VERSION
2.3.0

##### SUMMARY
 The newer vmware_guest module does not currently support loading / managing cd_roms.  The older vsphere_guest has this functionality; it would be nice to have it ported forward so we can do something like the following.

```
    hardware:
      memory_mb: 2048
      num_cpus: 2
      scsi: paravirtual
      vm_cdrom:
        type: "iso"
        iso_path: "DatastoreName/cd-image.iso"
```


Discussion:
cc @jctanner @nerzhul @dav1x
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
needs_contributor
Also wait for that feature
For what it's worth, VMWare has done a pretty good chunk of the work with an example: https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/cdrom_vm.py

I tried to poke at it, but pretty bogged down at the moment ):
cc @dagwieers
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
cc @akasurde
[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
@@ -25,6 +25,7 @@
 author:
 - James Tanner (@jctanner) <tanner.jc@gmail.com>
 - Loic Blot (@nerzhul) <loic.blot@unix-experience.fr>
+- Philippe Dellaert (@pdellaert) <philippe@dellaert.org>
 notes:
 - Tested on vSphere 5.5 and 6.0
 requirements:
@@ -64,7 +65,7 @@
     version_added: '2.3'
   folder:
     description:
-    - Destination folder, absolute or relative path to find an existing guest or create the new guest.
+    - Destination folder, absolute path to find an existing guest or create the new guest.
     - The folder should include the datacenter. ESX's datacenter is ha-datacenter
     - 'Examples:'
     - '   folder: /ha-datacenter/vm'
@@ -76,8 +77,6 @@
     - '   folder: /folder1/datacenter1/vm'
     - '   folder: folder1/datacenter1/vm'
     - '   folder: /folder1/datacenter1/vm/folder2'
-    - '   folder: vm/folder2'
-    - '   folder: folder2'
     default: /vm
   hardware:
     description:
@@ -102,6 +101,13 @@
     - ' - C(type) (string): Valid value is C(thin) (default: None).'
     - ' - C(datastore) (string): Datastore to use for the disk. If C(autoselect_datastore) is enabled, filter datastore selection.'
     - ' - C(autoselect_datastore) (bool): select the less used datastore.'
+  cdrom:
+    description:
+    - A CD-ROM configuration for the VM.
+    - 'Valid attributes are:'
+    - ' - C(type) (string): The type of CD-ROM, valid options are C(none), C(client) or C(iso). With C(none) the CD-ROM will be disconnected but present.'
+    - ' - C(iso_path) (string): The datastore path to the ISO file to use, in the form of C([datastore1] path/to/file.iso). Required if type is iso.'
+    version_added: '2.5'
   resource_pool:
     description:
     - Affect machine to the given resource pool.
@@ -209,6 +215,9 @@
       memory_mb: 512
       num_cpus: 1
       scsi: paravirtual
+    cdrom:
+      type: iso
+      iso_path: "[datastore1] livecd.iso"
     networks:
     - name: VM Network
       mac: aa:bb:dd:aa:00:14
@@ -358,6 +367,52 @@ def is_scsi_controller(device):
             isinstance(device, vim.vm.device.VirtualBusLogicController) or \
             isinstance(device, vim.vm.device.VirtualLsiLogicSASController)
 
+    @staticmethod
+    def create_ide_controller():
+        ide_ctl = vim.vm.device.VirtualDeviceSpec()
+        ide_ctl.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
+        ide_ctl.device = vim.vm.device.VirtualIDEController()
+        ide_ctl.device.deviceInfo = vim.Description()
+        ide_ctl.device.busNumber = 0
+
+        return ide_ctl
+
+    @staticmethod
+    def create_cdrom(ide_ctl, cdrom_type, iso_path=None):
+        cdrom_spec = vim.vm.device.VirtualDeviceSpec()
+        cdrom_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
+        cdrom_spec.device = vim.vm.device.VirtualCdrom()
+        cdrom_spec.device.controllerKey = ide_ctl.device.key
+        cdrom_spec.device.key = -1
+        cdrom_spec.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo()
+        cdrom_spec.device.connectable.allowGuestControl = True
+        cdrom_spec.device.connectable.startConnected = (cdrom_type != "none")
+        if cdrom_type in ["none", "client"]:
+            cdrom_spec.device.backing = vim.vm.device.VirtualCdrom.RemotePassthroughBackingInfo()
+        elif cdrom_type == "iso":
+            cdrom_spec.device.backing = vim.vm.device.VirtualCdrom.IsoBackingInfo(fileName=iso_path)
+
+        return cdrom_spec
+
+    @staticmethod
+    def is_equal_cdrom(vm_obj, cdrom_device, cdrom_type, iso_path):
+        if cdrom_type == "none":
+            return (isinstance(cdrom_device.backing, vim.vm.device.VirtualCdrom.RemotePassthroughBackingInfo) and
+                    cdrom_device.connectable.allowGuestControl and
+                    not cdrom_device.connectable.startConnected and
+                    (vm_obj.runtime.powerState != vim.VirtualMachinePowerState.poweredOn or not cdrom_device.connectable.connected))
+        elif cdrom_type == "client":
+            return (isinstance(cdrom_device.backing, vim.vm.device.VirtualCdrom.RemotePassthroughBackingInfo) and
+                    cdrom_device.connectable.allowGuestControl and
+                    cdrom_device.connectable.startConnected and
+                    (vm_obj.runtime.powerState != vim.VirtualMachinePowerState.poweredOn or cdrom_device.connectable.connected))
+        elif cdrom_type == "iso":
+            return (isinstance(cdrom_device.backing, vim.vm.device.VirtualCdrom.IsoBackingInfo) and
+                    cdrom_device.backing.fileName == iso_path and
+                    cdrom_device.connectable.allowGuestControl and
+                    cdrom_device.connectable.startConnected and
+                    (vm_obj.runtime.powerState != vim.VirtualMachinePowerState.poweredOn or cdrom_device.connectable.connected))
+
     def create_scsi_disk(self, scsi_ctl, disk_index=None):
         diskspec = vim.vm.device.VirtualDeviceSpec()
         diskspec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
@@ -527,7 +582,7 @@ def configure_guestid(self, vm_obj, vm_creation=False):
         if vm_creation and self.params['guest_id'] is None:
             self.module.fail_json(msg="guest_id attribute is mandatory for VM creation")
 
-        if vm_obj is None or self.params['guest_id'] != vm_obj.summary.config.guestId:
+        if self.params['guest_id'] and (vm_obj is None or self.params['guest_id'] != vm_obj.summary.config.guestId):
             self.change_detected = True
             self.configspec.guestId = self.params['guest_id']
 
@@ -550,6 +605,76 @@ def configure_cpu_and_memory(self, vm_obj, vm_creation=False):
             elif vm_creation and not self.params['template']:
                 self.module.fail_json(msg="hardware.memory_mb attribute is mandatory for VM creation")
 
+    def configure_cdrom(self, vm_obj):
+        # Configure the VM CD-ROM
+        if "cdrom" in self.params and self.params["cdrom"]:
+            if "type" not in self.params["cdrom"] or self.params["cdrom"]["type"] not in ["none", "client", "iso"]:
+                self.module.fail_json(msg="cdrom.type is mandatory")
+            if self.params["cdrom"]["type"] == "iso" and ("iso_path" not in self.params["cdrom"] or not self.params["cdrom"]["iso_path"]):
+                self.module.fail_json(msg="cdrom.iso_path is mandatory in case cdrom.type is iso")
+
+            if vm_obj and vm_obj.config.template:
+                # Changing CD-ROM settings on a template is not supported
+                return
+
+            cdrom_spec = None
+            cdrom_device = self.get_vm_cdrom_device(vm=vm_obj)
+            iso_path = self.params["cdrom"]["iso_path"] if "iso_path" in self.params["cdrom"] else None
+            if cdrom_device is None:
+                # Creating new CD-ROM
+                ide_device = self.get_vm_ide_device(vm=vm_obj)
+                if ide_device is None:
+                    # Creating new IDE device
+                    ide_device = self.device_helper.create_ide_controller()
+                    self.change_detected = True
+                    self.configspec.deviceChange.append(ide_device)
+                elif len(ide_device.device) > 3:
+                    self.module.fail_json(msg="hardware.cdrom specified for a VM or template which already has 4 IDE devices of which none are a cdrom")
+
+                cdrom_spec = self.device_helper.create_cdrom(ide_ctl=ide_device, cdrom_type=self.params["cdrom"]["type"], iso_path=iso_path)
+                if vm_obj and vm_obj.runtime.powerState == vim.VirtualMachinePowerState.poweredOn:
+                    cdrom_spec.device.connectable.connected = (self.params["cdrom"]["type"] != "none")
+
+            elif not self.device_helper.is_equal_cdrom(vm_obj=vm_obj, cdrom_device=cdrom_device, cdrom_type=self.params["cdrom"]["type"], iso_path=iso_path):
+                # Updating an existing CD-ROM
+                if self.params["cdrom"]["type"] in ["client", "none"]:
+                    cdrom_device.backing = vim.vm.device.VirtualCdrom.RemotePassthroughBackingInfo()
+                elif self.params["cdrom"]["type"] == "iso":
+                    cdrom_device.backing = vim.vm.device.VirtualCdrom.IsoBackingInfo(fileName=iso_path)
+                cdrom_device.connectable = vim.vm.device.VirtualDevice.ConnectInfo()
+                cdrom_device.connectable.allowGuestControl = True
+                cdrom_device.connectable.startConnected = (self.params["cdrom"]["type"] != "none")
+                if vm_obj and vm_obj.runtime.powerState == vim.VirtualMachinePowerState.poweredOn:
+                    cdrom_device.connectable.connected = (self.params["cdrom"]["type"] != "none")
+
+                cdrom_spec = vim.vm.device.VirtualDeviceSpec()
+                cdrom_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
+                cdrom_spec.device = cdrom_device
+
+            if cdrom_spec:
+                self.change_detected = True
+                self.configspec.deviceChange.append(cdrom_spec)
+
+    def get_vm_cdrom_device(self, vm=None):
+        if vm is None:
+            return None
+
+        for device in vm.config.hardware.device:
+            if isinstance(device, vim.vm.device.VirtualCdrom):
+                return device
+
+        return None
+
+    def get_vm_ide_device(self, vm=None):
+        if vm is None:
+            return None
+
+        for device in vm.config.hardware.device:
+            if isinstance(device, vim.vm.device.VirtualIDEController):
+                return device
+
+        return None
+
     def get_vm_network_interfaces(self, vm=None):
         if vm is None:
             return []
@@ -1148,6 +1273,7 @@ def deploy_vm(self):
         self.configure_cpu_and_memory(vm_obj=vm_obj, vm_creation=True)
         self.configure_disks(vm_obj=vm_obj)
         self.configure_network(vm_obj=vm_obj)
+        self.configure_cdrom(vm_obj=vm_obj)
 
         # Find if we need network customizations (find keys in dictionary that requires customizations)
         network_changes = False
@@ -1265,6 +1391,7 @@ def reconfigure_vm(self):
         self.configure_cpu_and_memory(vm_obj=self.current_vm_obj)
         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.customize_customvalues(vm_obj=self.current_vm_obj)
 
         if self.params['annotation'] and self.current_vm_obj.config.annotation != self.params['annotation']:
@@ -1306,7 +1433,7 @@ def reconfigure_vm(self):
                 return {'changed': change_applied, 'failed': True, 'msg': task.info.error.msg}
 
         # Mark VM as Template
-        if self.params['is_template']:
+        if self.params['is_template'] and not self.current_vm_obj.config.template:
             self.current_vm_obj.MarkAsTemplate()
             change_applied = True
 
@@ -1352,6 +1479,7 @@ def main():
         folder=dict(type='str', default='/vm'),
         guest_id=dict(type='str'),
         disk=dict(type='list', default=[]),
+        cdrom=dict(type='dict', default={}),
         hardware=dict(type='dict', default={}),
         force=dict(type='bool', default=False),
         datacenter=dict(type='str', default='ha-datacenter'),


Test patch:
diff --git a/test/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f0.yml b/test/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f0.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/vmware_guest/tasks/cdrom_d1_c1_f0.yml
@@ -0,0 +1,127 @@
+- name: Wait for Flask controller to come up online
+  wait_for:
+    host: "{{ vcsim }}"
+    port: 5000
+    state: started
+
+- name: kill vcsim
+  uri:
+    url: "{{ 'http://' + vcsim + ':5000/killall' }}"
+- name: start vcsim with no folders
+  uri:
+    url: "{{ 'http://' + vcsim + ':5000/spawn?datacenter=1&cluster=1&folder=0' }}"
+  register: vcsim_instance
+
+- name: Wait for Flask controller to come up online
+  wait_for:
+    host: "{{ vcsim }}"
+    port: 443
+    state: started
+
+- name: get a list of Clusters from vcsim
+  uri:
+    url: "{{ 'http://' + vcsim + ':5000/govc_find?filter=CCR' }}"
+  register: clusterlist
+
+- debug: var=vcsim_instance
+- debug: var=clusterlist
+
+- name: Create VM with CDROM
+  vmware_guest:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    folder: "/{{ (clusterlist['json'][0]|basename).split('_')[0] }}/vm"
+    name: CDROM-Test
+    datacenter: "{{ (clusterlist['json'][0]|basename).split('_')[0] }}"
+    cluster: "{{ clusterlist['json'][0] }}"
+    resource_pool: Resources
+    guest_id: centos64Guest
+    hardware:
+      memory_mb: 512
+      num_cpus: 1
+      scsi: paravirtual
+    disk:
+    - size_mb: 128
+      type: thin
+      datastore: LocalDS_0
+    cdrom:
+      type: iso
+      iso_path: "[LocalDS_0] base.iso"
+  register: cdrom_vm
+
+- debug: var=cdrom_vm
+
+- name: assert the VM was created
+  assert:
+    that:
+      - "cdrom_vm.failed == false"
+      - "cdrom_vm.changed == true"
+
+- name: Update CDROM to iso for the new VM
+  vmware_guest:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    folder: "/{{ (clusterlist['json'][0]|basename).split('_')[0] }}/vm"
+    name: CDROM-Test
+    datacenter: "{{ (clusterlist['json'][0]|basename).split('_')[0] }}"
+    cdrom:
+      type: iso
+      iso_path: "[LocalDS_0] base_new.iso"
+    state: present
+  register: cdrom_vm
+
+- debug: var=cdrom_vm
+
+- name: assert the VM was changed
+  assert:
+    that:
+      - "cdrom_vm.failed == false"
+      - "cdrom_vm.changed == true"
+
+- name: Update CDROM to client for the new VM
+  vmware_guest:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    folder: "/{{ (clusterlist['json'][0]|basename).split('_')[0] }}/vm"
+    name: CDROM-Test
+    datacenter: "{{ (clusterlist['json'][0]|basename).split('_')[0] }}"
+    cdrom:
+      type: client
+    state: present
+  register: cdrom_vm
+
+- debug: var=cdrom_vm
+
+- name: assert the VM was changed
+  assert:
+    that:
+      - "cdrom_vm.failed == false"
+      - "cdrom_vm.changed == true"
+
+- name: Update CDROM to none for the new VM
+  vmware_guest:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    folder: "/{{ (clusterlist['json'][0]|basename).split('_')[0] }}/vm"
+    name: CDROM-Test
+    datacenter: "{{ (clusterlist['json'][0]|basename).split('_')[0] }}"
+    cdrom:
+      type: none
+    state: present
+  register: cdrom_vm
+
+- debug: var=cdrom_vm
+
+- name: assert the VM was changed
+  assert:
+    that:
+      - "cdrom_vm.failed == false"
+      - "cdrom_vm.changed == true"
diff --git a/test/integration/targets/vmware_guest/tasks/main.yml b/test/integration/targets/vmware_guest/tasks/main.yml
--- a/test/integration/targets/vmware_guest/tasks/main.yml
+++ b/test/integration/targets/vmware_guest/tasks/main.yml
@@ -13,3 +13,4 @@
 - include: poweroff_d1_c1_f1.yml
 - include: clone_d1_c1_f0.yml
 - include: create_d1_c1_f0.yml
+- include: cdrom_d1_c1_f0.yml