This is an automated email from the ASF dual-hosted git repository.

RosiKyu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new ac65e3ddab0 server,kvm: Avoid device id conflicts between data 
volumes, CD-ROMs and the config drive ISO (#14407)
ac65e3ddab0 is described below

commit ac65e3ddab0b200965f83c77d5e960d3c613764b
Author: Wei Zhou <[email protected]>
AuthorDate: Wed Oct 28 10:40:02 2026 +0100

    server,kvm: Avoid device id conflicts between data volumes, CD-ROMs and the 
config drive ISO (#14407)
    
    * server: Reserve CD-ROM and config drive device ids for volumes
    
    Data volumes could be assigned the same device id as one of the CD-ROM
    devices or the config drive ISO, which end up with the same libvirt target
    name (e.g. sde) when they are on the same bus namespace (SATA with
    UEFI/Q35). With multiple CD-ROM support, the KVM agent defines all the
    CD-ROMs supported by the host at boot, so any slot after the primary one
    can conflict.
    
    Add VolumeApiService.getReservedDeviceIds(), which returns the device ids
    of the CD-ROM devices and of the config drive ISO when the Instance uses
    config drive. The number of CD-ROM devices is returned by
    TemplateManager.getVmCdromCount(), based on the CD-ROM cap advertised by the
    host and the ISOs attached. The config drive is detected from the network
    providers (Dhcp, Dns, UserData), so Instances which have never been started
    are covered as well as the ones with the config drive location detail.
    
    The reserved ids are skipped when allocating device ids in:
    - attach volume
    - restore of volumes and Instances from backup
    - import of Instances (unmanaged Instances and VMware to KVM)
    
    The max number of data volumes is preserved when device ids are reserved,
    and the error message of an invalid deviceId lists the reserved ids.
    
    * server,kvm: Calculate the device id of the config drive ISO
    
    The config drive ISO is attached as a CD-ROM, but its device id was
    hardcoded to 4, which is also the device id of the second ISO CD-ROM when
    multiple ISOs are supported. With vm.iso.max.count >= 2 the Instance could
    get two CD-ROMs on the same device id.
    
    The config drive ISO is now the CD-ROM following the ones of the ISOs, so
    its device id is the primary CD-ROM device id plus the number of the CD-ROMs
    of the ISOs (TemplateManager.getConfigDriveDeviceId). As the config drive
    ISO takes one of the CD-ROMs supported by the host, the maximum number of
    ISOs of an Instance using config drive is lowered by one.
    
    The number of CD-ROM devices of an Instance 
(TemplateManager.getVmCdromCount)
    includes the config drive ISO, so the device ids reserved for the volumes
    follow it, and the volume device id allocators do not need to know about the
    config drive any more.
    
    The KVM agent finds the config drive CD-ROM by the path of its ISO, and 
keeps
    its label and bus when refreshing it, instead of using the hardcoded id 4 
and
    the label hdd (which does not match the SATA CD-ROMs of UEFI Instances).
    
    * server,kvm: Do not count the config drive ISO in vm.iso.max.count
    
    vm.iso.max.count is the maximum number of ISOs of an Instance, so it should
    not take the config drive ISO into account: the config drive ISO is attached
    as an additional CD-ROM, following the ones of the ISOs.
    
    As the IDE bus only has the slots of the first MAX_CDROMS_PER_VM CD-ROMs
    (hdc and hdd), the KVM agent attaches the CD-ROMs beyond these slots (e.g. 
the
    config drive ISO following the maximum number of ISOs) on the SATA bus.
    
    * Revert "server,kvm: Calculate the device id of the config drive ISO"
    
    The config drive ISO keeps the device id 4, whatever the number of CD-ROMs
    of the Instance. The CD-ROMs of the ISOs are attached around it.
    
    This reverts the commits:
    - server,kvm: Do not count the config drive ISO in vm.iso.max.count
    - server,kvm: Calculate the device id of the config drive ISO
    
    * server,kvm: Skip the device id of the config drive ISO for the ISOs
    
    The config drive ISO keeps the device id 4, whatever the number of ISOs of
    the Instance. When an Instance uses config drive, the CD-ROMs of the ISOs
    which would use that device id or the next ones are moved to the next device
    ids, i.e. the CD-ROMs are, by device id: ISO 1 (3), config drive (4),
    ISO 2 (5), ISO 3 (6), ...
    
    The ISO slots stored for the Instance (user_vm.iso_id and vm_iso_map) are 
not
    changed. They are converted to device ids when the CD-ROMs are defined for 
the
    Instance, and when an ISO is attached or detached, so Instances which 
already
    have an ISO in the slot following the first one do not need to be updated.
    
    The number of CD-ROM devices of an Instance 
(TemplateManager.getVmCdromCount)
    includes the one of the config drive ISO, so the device ids reserved for the
    volumes cover all of them. When the networks of an Instance are not known 
yet
    (import, restore from backup), the config drive is always assumed.
    
    CloudStack supports 2 CD-ROMs per Instance on the IDE bus (hdc and hdd, the
    first two slots are kept for the disks). The KVM agent defines the CD-ROMs
    beyond these slots on the SATA bus.
    
    * server: Return the device ids of the CD-ROMs instead of their number
    
    TemplateManager exposed the number of CD-ROMs of an Instance, which the only
    caller (VolumeApiServiceImpl) converted back to the device ids starting from
    the primary CD-ROM device id. Return the device ids directly
    (TemplateManager.getCdromDeviceIds), so the callers do not have to assume
    that they are consecutive, and the number of CD-ROMs becomes an internal
    detail of TemplateManagerImpl.
---
 .../java/com/cloud/storage/VolumeApiService.java   |  18 +++
 .../java/com/cloud/template/TemplateManager.java   |  20 +++
 .../hypervisor/kvm/resource/LibvirtVMDef.java      |  16 +-
 .../hypervisor/kvm/resource/LibvirtVMDefTest.java  |  21 +++
 .../main/java/com/cloud/hypervisor/KVMGuru.java    |  31 ++--
 .../network/element/ConfigDriveNetworkElement.java |   2 +-
 .../com/cloud/storage/VolumeApiServiceImpl.java    |  44 ++++--
 .../com/cloud/template/TemplateManagerImpl.java    |  94 ++++++++++-
 .../cloudstack/vm/UnmanagedVMsManagerImpl.java     |  25 ++-
 .../java/com/cloud/hypervisor/KVMGuruTest.java     |  87 +++++++++-
 .../cloud/storage/VolumeApiServiceImplTest.java    | 114 ++++++++++++++
 .../cloud/template/TemplateManagerImplTest.java    | 175 +++++++++++++++++++++
 .../cloudstack/vm/UnmanagedVMsManagerImplTest.java |  24 +++
 13 files changed, 634 insertions(+), 37 deletions(-)

diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java 
b/api/src/main/java/com/cloud/storage/VolumeApiService.java
index 372eb038561..78bc106e00c 100644
--- a/api/src/main/java/com/cloud/storage/VolumeApiService.java
+++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java
@@ -21,6 +21,7 @@ package com.cloud.storage;
 import java.net.MalformedURLException;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import com.cloud.dc.DataCenter;
 import com.cloud.exception.ResourceAllocationException;
@@ -28,6 +29,7 @@ import com.cloud.offering.DiskOffering;
 import com.cloud.user.Account;
 import com.cloud.utils.Pair;
 import com.cloud.utils.fsm.NoTransitionException;
+import com.cloud.vm.VirtualMachine;
 
 import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd;
 import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd;
@@ -187,6 +189,22 @@ public interface VolumeApiService {
      */
     boolean doesStoragePoolSupportDiskOfferingTags(StoragePool destPool, 
String diskOfferingTags);
 
+    /**
+     * Returns the device ids of the Instance which are used by non-volume 
devices (the CD-ROM drives and the config drive ISO),
+     * and therefore must not be assigned to its volumes. Otherwise the 
hypervisor may end up with two devices using the same
+     * target name (e.g. sde), which is rejected.
+     *
+     * @param vm the Instance
+     * @return the reserved device ids
+     */
+    Set<Long> getReservedDeviceIds(VirtualMachine vm);
+
+    /**
+     * Same as {@link #getReservedDeviceIds(VirtualMachine)}, with the 
specified usage of config drive instead of the one
+     * of the Instance. It is useful when the networks of the Instance are not 
known yet, e.g. when importing it.
+     */
+    Set<Long> getReservedDeviceIds(VirtualMachine vm, boolean configDriveUsed);
+
     boolean validateConditionsToReplaceDiskOfferingOfVolume(Volume volume, 
DiskOffering newDiskOffering, StoragePool destPool);
 
     Volume destroyVolume(long volumeId, Account caller, boolean expunge, 
boolean forceExpunge, Boolean countDisplayFalseInResourceCount);
diff --git 
a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java 
b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java
index 8c11fe6c93a..4b5ca499937 100644
--- 
a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java
+++ 
b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java
@@ -17,6 +17,7 @@
 package com.cloud.template;
 
 import java.util.List;
+import java.util.Set;
 
 import org.apache.cloudstack.api.BaseCmd;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
@@ -37,6 +38,7 @@ import com.cloud.storage.VMTemplateStoragePoolVO;
 import com.cloud.storage.VMTemplateVO;
 import com.cloud.storage.VolumeVO;
 import com.cloud.utils.Pair;
+import com.cloud.vm.VirtualMachine;
 import com.cloud.vm.VirtualMachineProfile;
 
 /**
@@ -183,6 +185,24 @@ public interface TemplateManager {
      */
     void prepareIsoForVmProfile(VirtualMachineProfile profile, 
DeployDestination dest);
 
+    /**
+     * Returns the device ids of the CD-ROM devices of the Instance, starting 
from {@link #CDROM_PRIMARY_DEVICE_SEQ}.
+     * They cannot be used by the volumes of the Instance.
+     * The hypervisor defines all the CD-ROM devices it supports for the 
Instance (see {@code host.cdrom.max.count}),
+     * whether or not ISOs are attached to them. If the Instance uses config 
drive, the config drive ISO is one more
+     * CD-ROM: it keeps its device id (4), and the CD-ROMs of the ISOs from 
that device id are moved to the next ones.
+     *
+     * @param vm the Instance
+     * @return the device ids of the CD-ROM devices, including the one of the 
config drive ISO if the Instance uses config drive
+     */
+    Set<Long> getCdromDeviceIds(VirtualMachine vm);
+
+    /**
+     * Same as {@link #getCdromDeviceIds(VirtualMachine)}, with the specified 
usage of config drive instead of the one of
+     * the Instance. It is useful when the networks of the Instance are not 
known yet, e.g. when importing it.
+     */
+    Set<Long> getCdromDeviceIds(VirtualMachine vm, boolean configDriveUsed);
+
     public static final String MESSAGE_REGISTER_PUBLIC_TEMPLATE_EVENT = 
"Message.RegisterPublicTemplate.Event";
     public static final String MESSAGE_RESET_TEMPLATE_PERMISSION_EVENT = 
"Message.ResetTemplatePermission.Event";
 
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java
index b6b03bc4e11..4a1aa0ee924 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java
@@ -39,6 +39,7 @@ import com.cloud.agent.api.to.VirtualMachineMetadataTO;
 
 import com.cloud.agent.properties.AgentProperties;
 import com.cloud.agent.properties.AgentPropertiesFileHandler;
+import com.cloud.template.TemplateManager;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
@@ -57,8 +58,10 @@ import static 
java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME;
 public class LibvirtVMDef {
     protected static Logger LOGGER = LogManager.getLogger(LibvirtVMDef.class);
 
-    // CD-ROM slot allocation: getDevLabel() maps deviceSeq=3,4 to hdc and hdd 
on the IDE bus.
-    // Bumping this requires extending getDevLabel() (e.g. to spill onto SATA 
or a second IDE controller).
+    // Number of CD-ROMs supported by CloudStack per VM (not a limit of the 
guest OS or QEMU). The CD-ROMs are on the IDE bus,
+    // which has 4 slots: the first two are kept for disks, so the ISOs get 
hdc and hdd (getDevLabel() maps deviceSeq=3,4).
+    // Going past 2 means putting the CD-ROMs on the SATA bus (or on a second 
IDE controller): the CD-ROMs with a device id
+    // beyond these slots, e.g. the one following the config drive ISO (which 
keeps the device id 4), are on the SATA bus.
     public static final int MAX_CDROMS_PER_VM = 2;
 
     private String _hvsType;
@@ -1147,7 +1150,16 @@ public class LibvirtVMDef {
             _bus = DiskBus.IDE;
         }
 
+        private boolean isIdeCdromSlot(Integer devId) {
+            return devId == null || devId < 
TemplateManager.CDROM_PRIMARY_DEVICE_SEQ + MAX_CDROMS_PER_VM;
+        }
+
         public void defISODisk(String volPath, Integer devId, boolean 
isSecure, DiskType diskType) {
+            // the IDE bus only has the slots of MAX_CDROMS_PER_VM CD-ROMs 
(hdc and hdd), the other ones (e.g. the config drive ISO)
+            // are on the SATA bus
+            if (!isSecure && !isIdeCdromSlot(devId)) {
+                isSecure = true;
+            }
             if (!isSecure) {
                 defISODisk(volPath, devId, diskType);
             } else {
diff --git 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java
 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java
index 56ad267eac7..a51719a3326 100644
--- 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java
+++ 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java
@@ -597,4 +597,25 @@ public class LibvirtVMDefTest extends TestCase {
                 "<backend type='emulator' version='2.0'/>\n" +
                 "</tpm>\n", tpmDef.toString());
     }
+
+    @Test
+    public void testIsoDiskDefOnIdeBusForTheIdeCdromSlots() {
+        for (int[] slot : new int[][] {{3, 'c'}, {4, 'd'}}) {
+            DiskDef cdrom = new DiskDef();
+            cdrom.defISODisk(null, slot[0], false, DiskDef.DiskType.FILE);
+
+            assertEquals(DiskDef.DiskBus.IDE, cdrom.getBusType());
+            assertEquals("hd" + (char) slot[1], cdrom.getDiskLabel());
+        }
+    }
+
+    @Test
+    public void testIsoDiskDefBeyondTheIdeCdromSlotsIsOnSataBus() {
+        // e.g. the config drive ISO following the ISOs: IDE only has the hdc 
and hdd slots for CD-ROMs
+        DiskDef cdrom = new DiskDef();
+        cdrom.defISODisk("/mnt/pool/configdrive/i-2-3-VM.iso", 5, false, 
DiskDef.DiskType.FILE);
+
+        assertEquals(DiskDef.DiskBus.SATA, cdrom.getBusType());
+        assertEquals("sdf", cdrom.getDiskLabel());
+    }
 }
diff --git a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java 
b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java
index bdf52407841..5e3beed769a 100644
--- a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java
+++ b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java
@@ -58,6 +58,7 @@ import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
 
 public class KVMGuru extends HypervisorGuruBase implements HypervisorGuru {
@@ -91,30 +92,29 @@ public class KVMGuru extends HypervisorGuruBase implements 
HypervisorGuru {
      * Get next free DeviceId for a KVM Guest
      */
 
-    protected Long getNextAvailableDeviceId(List<VolumeVO> vmVolumes) {
+    protected Long getNextAvailableDeviceId(Set<Long> reservedDeviceIds, 
List<VolumeVO> vmVolumes) {
 
         int maxDataVolumesSupported;
-        int maxDeviceId;
-        List<String> devIds = new ArrayList<>();
+        List<Long> devIds = new ArrayList<>();
 
         try {
             maxDataVolumesSupported = 
_hypervisorCapabilitiesDao.getMaxDataVolumesLimit(HypervisorType.KVM,"default");
-            int maxDevices = maxDataVolumesSupported + 2; // add 2 to consider 
devices root volume and cdrom
-            maxDeviceId = maxDevices - 1;
         } catch (Exception e) {
             throw new RuntimeException("Cannot find maximum number of disk 
devices that can be attached to the KVM Hypervisor");
         }
-        for (int i = 1; i <= maxDeviceId; i++) {
-            devIds.add(String.valueOf(i));
+        // the device ids reserved for the CD-ROM and config drive devices 
cannot be used by volumes
+        for (long id = 1; devIds.size() < maxDataVolumesSupported; id++) {
+            if (!reservedDeviceIds.contains(id)) {
+                devIds.add(id);
+            }
         }
-        devIds.remove("3");
         for (VolumeVO vmVolume : vmVolumes) {
-            devIds.remove(vmVolume.getDeviceId().toString().trim());
+            devIds.remove(vmVolume.getDeviceId());
         }
         if (devIds.isEmpty()) {
             throw new RuntimeException("All device Ids are in use.");
         }
-        return Long.parseLong(devIds.iterator().next());
+        return devIds.get(0);
     }
 
 
@@ -373,6 +373,8 @@ public class KVMGuru extends HypervisorGuruBase implements 
HypervisorGuru {
                 vm.setPowerState(VirtualMachine.PowerState.PowerOff);
                 _instanceDao.update(vm.getId(), vm);
             }
+           // the networks of the (possibly expunged) VM may not be available, 
so always keep the config drive device id free
+           Set<Long> reservedDeviceIds = 
volumeApiService.getReservedDeviceIds(vm, true);
            for (Backup.VolumeInfo VMVolToRestore : vm.getBackupVolumeList()) {
                VolumeVO volume = 
_volumeDao.findByUuidIncludingRemoved(VMVolToRestore.getUuid());
                volume.setState(Volume.State.Ready);
@@ -388,9 +390,10 @@ public class KVMGuru extends HypervisorGuruBase implements 
HypervisorGuru {
                    // it here shifts every data disk by one slot on every 
restore, which then misaligns the
                    // device-id ordering that the restore itself uses to map 
backup files onto volumes.
                    Long deviceId = VMVolToRestore.getDeviceId();
-                   if (deviceId == null) {
-                       // Backups taken before the device id was recorded in 
the backup metadata.
-                       deviceId = 
getNextAvailableDeviceId(_volumeDao.findByInstance(vm.getId()));
+                   if (deviceId == null || 
reservedDeviceIds.contains(deviceId)) {
+                       // Backups taken before the device id was recorded in 
the backup metadata, or with a volume on a
+                       // device id which is reserved now for a CD-ROM or the 
config drive ISO.
+                       deviceId = getNextAvailableDeviceId(reservedDeviceIds, 
_volumeDao.findByInstance(vm.getId()));
                    }
                    _volumeDao.attachVolume(volume.getId(), vm.getId(), 
deviceId);
                }
@@ -415,7 +418,7 @@ public class KVMGuru extends HypervisorGuruBase implements 
HypervisorGuru {
         }
         if (restoredVolume != null) {
             try {
-                _volumeDao.attachVolume(restoredVolume.getId(), vm.getId(), 
getNextAvailableDeviceId(vmVolumes));
+                _volumeDao.attachVolume(restoredVolume.getId(), vm.getId(), 
getNextAvailableDeviceId(volumeApiService.getReservedDeviceIds(vm), vmVolumes));
                 restoredVolume.setState(Volume.State.Ready);
                 _volumeDao.update(restoredVolume.getId(), restoredVolume);
                 
UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_ATTACH, 
restoredVolume.getAccountId(), restoredVolume.getDataCenterId(), 
restoredVolume.getId(), restoredVolume.getName(),
diff --git 
a/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java 
b/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java
index 6cdd6f5753d..fbc0d106edc 100644
--- 
a/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java
+++ 
b/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java
@@ -142,7 +142,7 @@ public class ConfigDriveNetworkElement extends AdapterBase 
implements NetworkEle
     @Inject
     private HypervisorGuruManager _hvGuruMgr;
 
-    private final static Integer CONFIGDRIVEDISKSEQ = 4;
+    public static final Integer CONFIGDRIVEDISKSEQ = 4;
 
     private boolean canHandle(TrafficType trafficType) {
         return trafficType.equals(TrafficType.Guest);
diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java 
b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
index e81cc00564e..0702450d85c 100644
--- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
+++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
@@ -5358,14 +5358,39 @@ public class VolumeApiServiceImpl extends ManagerBase 
implements VolumeApiServic
         return StringUtils.isBlank(minHwVersion) ? defaultHypervisorVersion : 
minHwVersion;
     }
 
+    @Override
+    public Set<Long> getReservedDeviceIds(VirtualMachine vm) {
+        return _tmpltMgr.getCdromDeviceIds(vm);
+    }
+
+    @Override
+    public Set<Long> getReservedDeviceIds(VirtualMachine vm, boolean 
configDriveUsed) {
+        return _tmpltMgr.getCdromDeviceIds(vm, configDriveUsed);
+    }
+
+    /**
+     * Returns the first {@code maxDataVolumes} device ids (starting from 1) 
which are not reserved.
+     */
+    private List<Long> getAvailableDeviceIds(int maxDataVolumes, Set<Long> 
reservedDeviceIds) {
+        List<Long> availableDeviceIds = new ArrayList<>();
+        for (long id = 1; availableDeviceIds.size() < maxDataVolumes; id++) {
+            if (!reservedDeviceIds.contains(id)) {
+                availableDeviceIds.add(id);
+            }
+        }
+        return availableDeviceIds;
+    }
+
     private Long getDeviceId(UserVmVO vm, Long deviceId) {
         // allocate deviceId
-        int maxDevices = getMaxDataVolumesSupported(vm) + 2; // add 2 to 
consider devices root volume and cdrom
-        int maxDeviceId = maxDevices - 1;
+        Set<Long> reservedDeviceIds = getReservedDeviceIds(vm);
+        List<Long> availableDeviceIds = 
getAvailableDeviceIds(getMaxDataVolumesSupported(vm), reservedDeviceIds);
+        long maxDeviceId = availableDeviceIds.get(availableDeviceIds.size() - 
1);
         List<VolumeVO> vols = _volsDao.findByInstance(vm.getId());
         if (deviceId != null) {
-            if (deviceId.longValue() < 0 || deviceId.longValue() > maxDeviceId 
|| deviceId.longValue() == 3) {
-                throw new RuntimeException("deviceId should be 0,1,2,4-" + 
maxDeviceId);
+            if (deviceId.longValue() < 0 || deviceId.longValue() > maxDeviceId 
|| reservedDeviceIds.contains(deviceId)) {
+                throw new RuntimeException(String.format("deviceId should be 0 
or between 1 and %d, except the ones reserved for CD-ROM/config drive devices: 
%s",
+                        maxDeviceId, reservedDeviceIds));
             }
             for (VolumeVO vol : vols) {
                 if (vol.getDeviceId().equals(deviceId)) {
@@ -5374,18 +5399,13 @@ public class VolumeApiServiceImpl extends ManagerBase 
implements VolumeApiServic
             }
         } else {
             // allocate deviceId here
-            List<String> devIds = new ArrayList<String>();
-            for (int i = 1; i <= maxDeviceId; i++) {
-                devIds.add(String.valueOf(i));
-            }
-            devIds.remove("3");
             for (VolumeVO vol : vols) {
-                devIds.remove(vol.getDeviceId().toString().trim());
+                availableDeviceIds.remove(vol.getDeviceId());
             }
-            if (devIds.isEmpty()) {
+            if (availableDeviceIds.isEmpty()) {
                 throw new RuntimeException(String.format("All device Ids are 
used by vm %s", vm));
             }
-            deviceId = Long.parseLong(devIds.iterator().next());
+            deviceId = availableDeviceIds.get(0);
         }
 
         return deviceId;
diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java 
b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
index 873afff01c8..482d0b405d7 100755
--- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
+++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
@@ -24,8 +24,10 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -155,6 +157,10 @@ import com.cloud.hypervisor.Hypervisor;
 import com.cloud.hypervisor.Hypervisor.HypervisorType;
 import com.cloud.hypervisor.HypervisorGuru;
 import com.cloud.hypervisor.HypervisorGuruManager;
+import com.cloud.network.Network.Provider;
+import com.cloud.network.Network.Service;
+import com.cloud.network.NetworkModel;
+import com.cloud.network.element.ConfigDriveNetworkElement;
 import com.cloud.projects.Project;
 import com.cloud.projects.ProjectManager;
 import com.cloud.resourcelimit.CheckedReservation;
@@ -219,6 +225,7 @@ import com.cloud.utils.db.Transaction;
 import com.cloud.utils.db.TransactionCallbackNoReturn;
 import com.cloud.utils.db.TransactionStatus;
 import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.NicVO;
 import com.cloud.vm.UserVmManager;
 import com.cloud.vm.UserVmVO;
 import com.cloud.vm.VMInstanceVO;
@@ -228,9 +235,11 @@ import com.cloud.vm.VirtualMachineProfile;
 import com.cloud.vm.VirtualMachineProfileImpl;
 import com.cloud.vm.VmDetailConstants;
 import com.cloud.vm.VmIsoMapVO;
+import com.cloud.vm.dao.NicDao;
 import com.cloud.vm.dao.UserVmDao;
 import com.cloud.vm.dao.VmIsoMapDao;
 import com.cloud.vm.dao.VMInstanceDao;
+import com.cloud.vm.dao.VMInstanceDetailsDao;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 
@@ -265,6 +274,12 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
     @Inject
     private UserVmDao _userVmDao;
     @Inject
+    private NicDao _nicDao;
+    @Inject
+    private NetworkModel _networkModel;
+    @Inject
+    private VMInstanceDetailsDao _vmInstanceDetailsDao;
+    @Inject
     private VmIsoMapDao _vmIsoMapDao;
     @Inject
     private VolumeDao _volumeDao;
@@ -705,15 +720,87 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
         // Pre-allocate every cdrom slot at boot. QEMU/IDE refuses to hot-add 
new cdrom drives, so
         // runtime attachIso can only media-swap into a slot the domain 
already owns.
         int totalSlots = Math.max(effectiveMaxCdroms(vm, 
dest.getHost().getId()), slotsNeededFor(slotToIsoId));
+        boolean configDriveUsed = isConfigDriveUsed(vm);
         for (int i = 0; i < totalSlots; i++) {
-            int diskSeq = CDROM_PRIMARY_DEVICE_SEQ + i;
-            Long isoId = slotToIsoId.get(diskSeq);
+            int slot = CDROM_PRIMARY_DEVICE_SEQ + i;
+            int diskSeq = getIsoDeviceSeq(slot, configDriveUsed);
+            Long isoId = slotToIsoId.get(slot);
             profile.addDisk(isoId != null
                     ? buildIsoDisk(profile, vm, dest, poolId, diskSeq, isoId)
                     : buildEmptyCdromDisk(diskSeq));
         }
     }
 
+    @Override
+    public Set<Long> getCdromDeviceIds(VirtualMachine vm) {
+        if (vm.getType() != VirtualMachine.Type.User) {
+            return getCdromDeviceIds(1);
+        }
+        return getCdromDeviceIds(vm, isConfigDriveUsed(vm));
+    }
+
+    @Override
+    public Set<Long> getCdromDeviceIds(VirtualMachine vm, boolean 
configDriveUsed) {
+        return getCdromDeviceIds(getCdromCount(vm, configDriveUsed));
+    }
+
+    private Set<Long> getCdromDeviceIds(int cdromCount) {
+        Set<Long> deviceIds = new HashSet<>();
+        for (int i = 0; i < cdromCount; i++) {
+            deviceIds.add((long) CDROM_PRIMARY_DEVICE_SEQ + i);
+        }
+        return deviceIds;
+    }
+
+    private int getCdromCount(VirtualMachine vm, boolean configDriveUsed) {
+        if (vm.getType() != VirtualMachine.Type.User) {
+            return 1;
+        }
+        // all the CD-ROM devices supported by the host are defined for the 
Instance at boot, even if the configured
+        // maximum number of ISOs (vm.iso.max.count) is lower or is increased 
later
+        int cdromCount = advertisedCdromCap(hostIdForVm(vm));
+        UserVmVO userVm = _userVmDao.findById(vm.getId());
+        if (userVm != null) {
+            cdromCount = Math.max(cdromCount, 
slotsNeededFor(loadAttachedIsoSlots(userVm)));
+        }
+        // the config drive ISO is one more CD-ROM, it is not counted in the 
CD-ROMs supported for the ISOs
+        return cdromCount + (configDriveUsed ? 1 : 0);
+    }
+
+    /**
+     * Returns the device id of the CD-ROM of an ISO slot (the slots are the 
ones stored for the ISOs of the Instance,
+     * starting from {@link #CDROM_PRIMARY_DEVICE_SEQ}). The config drive ISO 
always has the device id
+     * {@link ConfigDriveNetworkElement#CONFIGDRIVEDISKSEQ}, so the slots from 
it are moved to the next device ids
+     * when the Instance uses config drive.
+     */
+    int getIsoDeviceSeq(int slot, boolean configDriveUsed) {
+        return configDriveUsed && slot >= 
ConfigDriveNetworkElement.CONFIGDRIVEDISKSEQ ? slot + 1 : slot;
+    }
+
+    /**
+     * Checks if a config drive ISO is (or will be) attached to the Instance, 
which is the case when any of its networks
+     * has ConfigDrive as the provider of the Dhcp, Dns or UserData service. 
Checking the network providers (instead of
+     * only the config drive location detail, which is added when the ISO is 
created) also covers Instances which have
+     * never been started.
+     */
+    boolean isConfigDriveUsed(VirtualMachine vm) {
+        if (_vmInstanceDetailsDao.findDetail(vm.getId(), 
VmDetailConstants.CONFIG_DRIVE_LOCATION) != null) {
+            return true;
+        }
+        List<NicVO> nics = _nicDao.listByVmId(vm.getId());
+        if (CollectionUtils.isEmpty(nics)) {
+            return false;
+        }
+        for (NicVO nic : nics) {
+            for (Service service : Arrays.asList(Service.UserData, 
Service.Dhcp, Service.Dns)) {
+                if 
(_networkModel.isProviderSupportServiceInNetwork(nic.getNetworkId(), service, 
Provider.ConfigDrive)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     private Long singleStoragePoolId(DeployDestination dest) {
         Long poolId = null;
         Map<Volume, StoragePool> storageForDisks = dest.getStorageForDisks();
@@ -1451,7 +1538,8 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
         }
 
         DataTO isoTO = tmplt.getTO();
-        DiskTO disk = new DiskTO(isoTO, (long) deviceSeq, null, 
Volume.Type.ISO);
+        int diskSeq = !isVirtualRouter ? getIsoDeviceSeq(deviceSeq, 
isConfigDriveUsed(vm)) : deviceSeq;
+        DiskTO disk = new DiskTO(isoTO, (long) diskSeq, null, Volume.Type.ISO);
 
         HypervisorGuru hvGuru = _hvGuruMgr.getGuru(vm.getHypervisorType());
         VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm);
diff --git 
a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java 
b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
index 233c67d690f..c0e17a064a3 100644
--- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
@@ -940,6 +940,21 @@ public class UnmanagedVMsManagerImpl implements 
UnmanagedVMsManager {
         return new Pair<DiskProfile, StoragePool>(profile, storagePool);
     }
 
+    /**
+     * Returns the device ids which cannot be used by the volumes of the 
imported Instance: the ones of the CD-ROM devices,
+     * and the one of the config drive ISO (always kept free, as the NICs of 
the Instance are not imported yet at this point).
+     */
+    private Set<Long> getReservedDeviceIds(VirtualMachine vm) {
+        return volumeApiService.getReservedDeviceIds(vm, true);
+    }
+
+    private long getNextDeviceId(long deviceId, Set<Long> reservedDeviceIds) {
+        do {
+            deviceId++;
+        } while (reservedDeviceIds.contains(deviceId));
+        return deviceId;
+    }
+
     private NicProfile importNic(UnmanagedInstanceTO.Nic nic, VirtualMachine 
vm, Network network, Network.IpAddresses ipAddresses, int deviceId, boolean 
isDefaultNic, boolean forced) throws 
InsufficientVirtualNetworkCapacityException, 
InsufficientAddressCapacityException {
         DataCenterVO dataCenterVO = 
dataCenterDao.findById(network.getDataCenterId());
         Pair<NicProfile, Integer> result = 
networkOrchestrationService.importNic(nic.getMacAddress(), deviceId, network, 
isDefaultNic, vm, ipAddresses, dataCenterVO, forced);
@@ -1269,7 +1284,8 @@ public class UnmanagedVMsManagerImpl implements 
UnmanagedVMsManager {
                 DiskOfferingVO diskOffering = 
diskOfferingDao.findById(serviceOffering.getDiskOfferingId());
                 diskProfileStoragePoolList.add(importDisk(rootDisk, userVm, 
cluster, diskOffering, Volume.Type.ROOT, String.format("ROOT-%d", 
userVm.getId()),
                         rootDisk.getCapacity(), minIops, maxIops, template, 
owner, null, storagePoolId));
-                long deviceId = 1L;
+                Set<Long> reservedDeviceIds = getReservedDeviceIds(userVm);
+                long deviceId = getNextDeviceId(0L, reservedDeviceIds);
                 for (UnmanagedInstanceTO.Disk disk : dataDisks) {
                     if (disk.getCapacity() == null || disk.getCapacity() == 0) 
{
                         throw new 
InvalidParameterValueException(String.format("Disk ID: %s size is invalid", 
rootDisk.getDiskId()));
@@ -1278,7 +1294,7 @@ public class UnmanagedVMsManagerImpl implements 
UnmanagedVMsManager {
                     diskProfileStoragePoolList.add(importDisk(disk, userVm, 
cluster, offering, Volume.Type.DATADISK, String.format("DATA-%d-%s", 
userVm.getId(), disk.getDiskId()),
                             disk.getCapacity(), offering.getMinIops(), 
offering.getMaxIops(),
                             template, owner, deviceId, storagePoolId));
-                    deviceId++;
+                    deviceId = getNextDeviceId(deviceId, reservedDeviceIds);
                 }
             } catch (Exception e) {
                 logger.error(String.format("Failed to import volumes while 
importing vm: %s", displayName), e);
@@ -3439,7 +3455,8 @@ public class UnmanagedVMsManagerImpl implements 
UnmanagedVMsManager {
                 diskProfileStoragePoolList.add(importExternalDisk(rootDisk, 
userVm, dest, diskOffering, Volume.Type.ROOT,
                         template, null, remoteUrl, username, password, 
tmpPath, diskProfile));
 
-                long deviceId = 1L;
+                Set<Long> reservedDeviceIds = getReservedDeviceIds(userVm);
+                long deviceId = getNextDeviceId(0L, reservedDeviceIds);
                 diskSeq = 0;
                 for (UnmanagedInstanceTO.Disk disk : dataDisks) {
                     DiskProfile dataDiskProfile = dataDiskProfiles[diskSeq++];
@@ -3447,7 +3464,7 @@ public class UnmanagedVMsManagerImpl implements 
UnmanagedVMsManager {
 
                     diskProfileStoragePoolList.add(importExternalDisk(disk, 
userVm, dest, offering, Volume.Type.DATADISK,
                             template, deviceId, remoteUrl, username, password, 
tmpPath, dataDiskProfile));
-                    deviceId++;
+                    deviceId = getNextDeviceId(deviceId, reservedDeviceIds);
                 }
             } catch (Exception e) {
                 logger.error(String.format("Failed to import volumes while 
importing vm: %s", instanceName), e);
diff --git a/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java 
b/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java
index 9e85271c388..e42acd90e88 100644
--- a/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java
+++ b/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java
@@ -20,7 +20,9 @@ import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
 import java.util.List;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.backup.Backup;
@@ -47,6 +49,7 @@ import com.cloud.configuration.ConfigurationManagerImpl;
 import com.cloud.event.UsageEventUtils;
 import com.cloud.host.HostVO;
 import com.cloud.host.dao.HostDao;
+import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao;
 import com.cloud.hypervisor.kvm.dpdk.DpdkHelper;
 import com.cloud.offering.ServiceOffering;
 import com.cloud.service.ServiceOfferingDetailsVO;
@@ -56,6 +59,7 @@ import com.cloud.service.dao.ServiceOfferingDetailsDao;
 import com.cloud.storage.GuestOSHypervisorVO;
 import com.cloud.storage.GuestOSVO;
 import com.cloud.storage.Volume;
+import com.cloud.storage.VolumeApiService;
 import com.cloud.storage.VolumeVO;
 import com.cloud.storage.dao.VolumeDao;
 import com.cloud.storage.dao.GuestOSDao;
@@ -113,6 +117,12 @@ public class KVMGuruTest {
     @Mock
     DpdkHelper dpdkHelperMock;
 
+    @Mock
+    HypervisorCapabilitiesDao hypervisorCapabilitiesDaoMock;
+
+    @Mock
+    VolumeApiService volumeApiServiceMock;
+
     @Mock
     GuestOSVO guestOsVoMock;
 
@@ -519,6 +529,44 @@ public class KVMGuruTest {
         Assert.assertNull(actualQuota);
     }
 
+    private List<VolumeVO> mockVolumesWithDeviceIds(long... deviceIds) {
+        List<VolumeVO> volumes = new java.util.ArrayList<>();
+        for (long deviceId : deviceIds) {
+            VolumeVO volume = Mockito.mock(VolumeVO.class);
+            Mockito.when(volume.getDeviceId()).thenReturn(deviceId);
+            volumes.add(volume);
+        }
+        return volumes;
+    }
+
+    @Test
+    public void getNextAvailableDeviceIdSkipsReservedDeviceIds() {
+        
Mockito.when(hypervisorCapabilitiesDaoMock.getMaxDataVolumesLimit(Hypervisor.HypervisorType.KVM,
 "default")).thenReturn(6);
+        List<VolumeVO> volumes = mockVolumesWithDeviceIds(0L, 1L, 2L);
+
+        Assert.assertEquals(Long.valueOf(4L), 
guru.getNextAvailableDeviceId(new HashSet<>(Arrays.asList(3L)), volumes));
+        Assert.assertEquals(Long.valueOf(5L), 
guru.getNextAvailableDeviceId(new HashSet<>(Arrays.asList(3L, 4L)), volumes));
+        Assert.assertEquals(Long.valueOf(6L), 
guru.getNextAvailableDeviceId(new HashSet<>(Arrays.asList(3L, 4L, 5L)), 
volumes));
+    }
+
+    @Test
+    public void 
getNextAvailableDeviceIdKeepsMaxDataVolumesWhenDeviceIdsAreReserved() {
+        
Mockito.when(hypervisorCapabilitiesDaoMock.getMaxDataVolumesLimit(Hypervisor.HypervisorType.KVM,
 "default")).thenReturn(6);
+        Set<Long> reserved = new HashSet<>(Arrays.asList(3L, 4L));
+        // root + 5 data volumes (1, 2, 5, 6, 7): the 6th data volume is still 
allowed, and gets device id 8
+        List<VolumeVO> volumes = mockVolumesWithDeviceIds(0L, 1L, 2L, 5L, 6L, 
7L);
+
+        Assert.assertEquals(Long.valueOf(8L), 
guru.getNextAvailableDeviceId(reserved, volumes));
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void 
getNextAvailableDeviceIdThrowsExceptionWhenAllDeviceIdsAreUsed() {
+        
Mockito.when(hypervisorCapabilitiesDaoMock.getMaxDataVolumesLimit(Hypervisor.HypervisorType.KVM,
 "default")).thenReturn(6);
+        List<VolumeVO> volumes = mockVolumesWithDeviceIds(0L, 1L, 2L, 5L, 6L, 
7L, 8L);
+
+        guru.getNextAvailableDeviceId(new HashSet<>(Arrays.asList(3L, 4L)), 
volumes);
+    }
+
     /**
      * The restore maps backup files onto volumes by their device id ordering, 
so re-importing the
      * instance has to put every data disk back on the device id recorded in 
the backup. Handing out
@@ -552,6 +600,43 @@ public class KVMGuruTest {
 
         Mockito.verify(_volumeDao).attachVolume(10L, 42L, 0L);
         Mockito.verify(_volumeDao).attachVolume(11L, 42L, 5L);
-        Mockito.verify(guru, 
Mockito.never()).getNextAvailableDeviceId(Mockito.anyList());
+        Mockito.verify(guru, 
Mockito.never()).getNextAvailableDeviceId(Mockito.anySet(), Mockito.anyList());
+    }
+
+    @Test
+    public void 
testImportVirtualMachineFromBackupDoesNotReinstateReservedDeviceIds() {
+        String vmInternalName = "i-2-42-VM";
+        VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class);
+        Mockito.when(vmInstance.getId()).thenReturn(42L);
+        Mockito.when(vmInstance.getRemoved()).thenReturn(null);
+        
Mockito.when(_instanceDao.findVMByInstanceNameIncludingRemoved(vmInternalName)).thenReturn(vmInstance);
+        // the CD-ROM and the config drive ISO
+        Mockito.when(volumeApiServiceMock.getReservedDeviceIds(vmInstance, 
true)).thenReturn(new HashSet<>(Arrays.asList(3L, 4L)));
+        
Mockito.when(hypervisorCapabilitiesDaoMock.getMaxDataVolumesLimit(Hypervisor.HypervisorType.KVM,
 "default")).thenReturn(6);
+        List<VolumeVO> currentVolumes = mockVolumesWithDeviceIds(0L, 1L, 2L);
+        
Mockito.when(_volumeDao.findByInstance(42L)).thenReturn(currentVolumes);
+
+        Backup.VolumeInfo rootVolume = new Backup.VolumeInfo("root-uuid", 
"root-uuid", Volume.Type.ROOT, 1L, 0L, null, null, null);
+        // taken when the config drive device id was not reserved yet
+        Backup.VolumeInfo dataVolume = new Backup.VolumeInfo("data-uuid", 
"data-uuid", Volume.Type.DATADISK, 1L, 4L, null, null, null);
+        
Mockito.when(vmInstance.getBackupVolumeList()).thenReturn(List.of(rootVolume, 
dataVolume));
+
+        VolumeVO rootVolumeVO = Mockito.mock(VolumeVO.class);
+        Mockito.when(rootVolumeVO.getId()).thenReturn(10L);
+        VolumeVO dataVolumeVO = Mockito.mock(VolumeVO.class);
+        Mockito.when(dataVolumeVO.getId()).thenReturn(11L);
+        
Mockito.when(_volumeDao.findByUuidIncludingRemoved("root-uuid")).thenReturn(rootVolumeVO);
+        
Mockito.when(_volumeDao.findByUuidIncludingRemoved("data-uuid")).thenReturn(dataVolumeVO);
+        
Mockito.when(backupProviderMock.getName()).thenReturn("backupProviderMock");
+
+        Backup backup = Mockito.mock(Backup.class);
+
+        try (MockedStatic<UsageEventUtils> ignored = 
Mockito.mockStatic(UsageEventUtils.class)) {
+            guru.importVirtualMachineFromBackup(1L, 1L, 1L, 1L, 
vmInternalName, backup, backupProviderMock);
+        }
+
+        Mockito.verify(_volumeDao).attachVolume(10L, 42L, 0L);
+        // device id 4 is reserved, so the volume gets the next free one
+        Mockito.verify(_volumeDao).attachVolume(11L, 42L, 5L);
     }
 }
diff --git 
a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java 
b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
index 12258e2cc16..334bb3f659f 100644
--- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
+++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
@@ -37,6 +37,7 @@ import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
@@ -128,6 +129,7 @@ import com.cloud.storage.dao.StoragePoolTagsDao;
 import com.cloud.storage.dao.VMTemplateDao;
 import com.cloud.storage.dao.VolumeDao;
 import com.cloud.storage.snapshot.SnapshotManager;
+import com.cloud.template.TemplateManager;
 import com.cloud.user.Account;
 import com.cloud.user.AccountManager;
 import com.cloud.user.AccountVO;
@@ -164,6 +166,8 @@ public class VolumeApiServiceImplTest {
     @Mock
     private VolumeDataStoreDao volumeDataStoreDaoMock;
     @Mock
+    private TemplateManager templateManagerMock;
+    @Mock
     private VolumeDao volumeDaoMock;
     @Mock
     private BackupDao backupDaoMock;
@@ -2896,4 +2900,114 @@ public class VolumeApiServiceImplTest {
                     e.getMessage() != null && e.getMessage().contains("VM 
should be in"));
         }
     }
+
+    private Long invokeGetDeviceId(UserVmVO vm, Long deviceId) throws 
Exception {
+        java.lang.reflect.Method method = 
VolumeApiServiceImpl.class.getDeclaredMethod("getDeviceId", UserVmVO.class, 
Long.class);
+        method.setAccessible(true);
+        try {
+            return (Long) method.invoke(volumeApiServiceImpl, vm, deviceId);
+        } catch (InvocationTargetException e) {
+            throw (Exception) e.getCause();
+        }
+    }
+
+    private void mockCdromDeviceIds(UserVmVO vm, Long... deviceIds) {
+        
lenient().when(templateManagerMock.getCdromDeviceIds(vm)).thenReturn(new 
HashSet<>(Arrays.asList(deviceIds)));
+    }
+
+    private List<VolumeVO> mockVmVolumes(UserVmVO vm, long... deviceIds) {
+        List<VolumeVO> volumes = new ArrayList<>();
+        for (long deviceId : deviceIds) {
+            VolumeVO volume = Mockito.mock(VolumeVO.class);
+            lenient().when(volume.getDeviceId()).thenReturn(deviceId);
+            volumes.add(volume);
+        }
+        
lenient().when(volumeDaoMock.findByInstance(vm.getId())).thenReturn(volumes);
+        return volumes;
+    }
+
+    @Test
+    public void getReservedDeviceIdsReturnsTheDeviceIdsOfTheCdroms() {
+        UserVmVO vm = getMockedVm();
+
+        mockCdromDeviceIds(vm, 3L);
+        Assert.assertEquals(new HashSet<>(Arrays.asList(3L)), 
volumeApiServiceImpl.getReservedDeviceIds(vm));
+
+        // e.g. the CD-ROM and the config drive ISO, which is attached as the 
CD-ROM following the ones of the ISOs
+        mockCdromDeviceIds(vm, 3L, 4L);
+        Assert.assertEquals(new HashSet<>(Arrays.asList(3L, 4L)), 
volumeApiServiceImpl.getReservedDeviceIds(vm));
+
+        mockCdromDeviceIds(vm, 3L, 4L, 5L);
+        Assert.assertEquals(new HashSet<>(Arrays.asList(3L, 4L, 5L)), 
volumeApiServiceImpl.getReservedDeviceIds(vm));
+    }
+
+    @Test
+    public void getDeviceIdUsesDeviceId4WhenVmHasOneCdrom() throws Exception {
+        UserVmVO vm = getMockedVm();
+        mockCdromDeviceIds(vm, 3L);
+        mockVmVolumes(vm, 0L, 1L, 2L);
+
+        Assert.assertEquals(Long.valueOf(4L), invokeGetDeviceId(vm, null));
+        Assert.assertEquals(Long.valueOf(4L), invokeGetDeviceId(vm, 4L));
+    }
+
+    @Test
+    public void getDeviceIdSkipsDeviceIdsOfAllCdroms() throws Exception {
+        UserVmVO vm = getMockedVm();
+        mockCdromDeviceIds(vm, 3L, 4L);
+        mockVmVolumes(vm, 0L, 1L, 2L);
+
+        Assert.assertEquals(Long.valueOf(5L), invokeGetDeviceId(vm, null));
+    }
+
+    @Test
+    public void getDeviceIdRejectsReservedDeviceIds() throws Exception {
+        UserVmVO vm = getMockedVm();
+        mockCdromDeviceIds(vm, 3L, 4L);
+        mockVmVolumes(vm, 0L);
+
+        for (long reservedDeviceId : new long[] {3L, 4L}) {
+            try {
+                invokeGetDeviceId(vm, reservedDeviceId);
+                fail("deviceId " + reservedDeviceId + " should be rejected");
+            } catch (RuntimeException e) {
+                Assert.assertTrue(e.getMessage(), 
e.getMessage().contains("deviceId should be 0 or between 1 and"));
+            }
+        }
+    }
+
+    @Test
+    public void getDeviceIdKeepsMaxDataVolumesWhenDeviceIdsAreReserved() 
throws Exception {
+        UserVmVO vm = getMockedVm();
+        mockCdromDeviceIds(vm, 3L, 4L);
+        List<VolumeVO> volumes = mockVmVolumes(vm, 0L);
+
+        // 6 data volumes are supported by default
+        List<Long> allocated = new ArrayList<>();
+        for (int i = 0; i < 6; i++) {
+            Long deviceId = invokeGetDeviceId(vm, null);
+            allocated.add(deviceId);
+            VolumeVO volume = Mockito.mock(VolumeVO.class);
+            lenient().when(volume.getDeviceId()).thenReturn(deviceId);
+            volumes.add(volume);
+        }
+
+        Assert.assertEquals(Arrays.asList(1L, 2L, 5L, 6L, 7L, 8L), allocated);
+        try {
+            invokeGetDeviceId(vm, null);
+            fail("All device ids are used, an exception is expected");
+        } catch (RuntimeException e) {
+            Assert.assertTrue(e.getMessage(), e.getMessage().contains("All 
device Ids are used"));
+        }
+    }
+
+    @Test
+    public void 
getReservedDeviceIdsWithConfigDriveReservesTheCdromOfConfigDrive() {
+        UserVmVO vm = getMockedVm();
+        when(templateManagerMock.getCdromDeviceIds(vm, false)).thenReturn(new 
HashSet<>(Arrays.asList(3L, 4L)));
+        when(templateManagerMock.getCdromDeviceIds(vm, true)).thenReturn(new 
HashSet<>(Arrays.asList(3L, 4L, 5L)));
+
+        Assert.assertEquals(new HashSet<>(Arrays.asList(3L, 4L)), 
volumeApiServiceImpl.getReservedDeviceIds(vm, false));
+        Assert.assertEquals(new HashSet<>(Arrays.asList(3L, 4L, 5L)), 
volumeApiServiceImpl.getReservedDeviceIds(vm, true));
+    }
 }
diff --git 
a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java 
b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java
index 365f66e7411..61407a9d4c2 100755
--- a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java
+++ b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java
@@ -65,10 +65,12 @@ import 
org.springframework.core.type.classreading.MetadataReaderFactory;
 import org.springframework.core.type.filter.TypeFilter;
 
 import com.cloud.agent.AgentManager;
+import com.cloud.agent.api.to.DiskTO;
 import com.cloud.api.query.dao.SnapshotJoinDao;
 import com.cloud.api.query.dao.UserVmJoinDao;
 import com.cloud.api.query.vo.UserVmJoinVO;
 import com.cloud.dc.dao.DataCenterDao;
+import com.cloud.deploy.DeployDestination;
 import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao;
 import com.cloud.domain.dao.DomainDao;
 import com.cloud.event.dao.UsageEventDao;
@@ -82,6 +84,9 @@ import com.cloud.host.dao.HostDao;
 import com.cloud.host.dao.HostDetailsDao;
 import com.cloud.hypervisor.Hypervisor;
 import com.cloud.hypervisor.HypervisorGuruManager;
+import com.cloud.network.Network.Provider;
+import com.cloud.network.Network.Service;
+import com.cloud.network.NetworkModel;
 import com.cloud.projects.ProjectManager;
 import com.cloud.resourcelimit.CheckedReservation;
 import com.cloud.storage.DataStoreRole;
@@ -120,13 +125,19 @@ import com.cloud.utils.UriUtils;
 import com.cloud.utils.concurrency.NamedThreadFactory;
 import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.uservm.UserVm;
+import com.cloud.vm.NicVO;
 import com.cloud.vm.UserVmVO;
+import com.cloud.vm.VMInstanceDetailVO;
 import com.cloud.vm.VMInstanceVO;
 import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VirtualMachineProfile;
 import com.cloud.vm.VirtualMachine.State;
+import com.cloud.vm.VmDetailConstants;
 import com.cloud.vm.VmIsoMapVO;
+import com.cloud.vm.dao.NicDao;
 import com.cloud.vm.dao.UserVmDao;
 import com.cloud.vm.dao.VMInstanceDao;
+import com.cloud.vm.dao.VMInstanceDetailsDao;
 import com.cloud.vm.dao.VmIsoMapDao;
 
 import junit.framework.TestCase;
@@ -249,6 +260,12 @@ public class TemplateManagerImplTest extends TestCase {
 
     @Mock
     HostDetailsDao _hostDetailsDao;
+    @Mock
+    NicDao _nicDao;
+    @Mock
+    NetworkModel _networkModel;
+    @Mock
+    VMInstanceDetailsDao _vmInstanceDetailsDao;
 
     @Mock
     UserVmJoinDao _userVmJoinDao;
@@ -1312,4 +1329,162 @@ public class TemplateManagerImplTest extends TestCase {
             }
         }
     }
+
+    private java.util.Set<Long> ids(Long... deviceIds) {
+        return new java.util.HashSet<>(Arrays.asList(deviceIds));
+    }
+
+    private UserVmVO mockUserVmOnHostWithCdromCap(Long advertisedCap) {
+        UserVmVO vm = Mockito.mock(UserVmVO.class);
+        Mockito.when(vm.getId()).thenReturn(1L);
+        Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.User);
+        Mockito.when(vm.getHostId()).thenReturn(7L);
+        Mockito.when(_userVmDao.findById(1L)).thenReturn(vm);
+        if (advertisedCap != null) {
+            DetailVO detail = Mockito.mock(DetailVO.class);
+            
Mockito.when(detail.getValue()).thenReturn(String.valueOf(advertisedCap));
+            Mockito.when(_hostDetailsDao.findDetail(7L, 
Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail);
+        }
+        return vm;
+    }
+
+    @Test
+    public void getCdromDeviceIdsReturnsOnlyThePrimaryCdromForSystemVms() {
+        VirtualMachine vm = Mockito.mock(VirtualMachine.class);
+        
Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.SecondaryStorageVm);
+
+        Assert.assertEquals(ids(3L), templateManager.getCdromDeviceIds(vm));
+    }
+
+    @Test
+    public void 
getCdromDeviceIdsReturnsAllCdromsAdvertisedByHostRegardlessOfConfiguredMaximum()
 {
+        // vm.iso.max.count defaults to 1, but the host defines all the 
CD-ROMs it supports
+        UserVmVO vm = mockUserVmOnHostWithCdromCap(2L);
+
+        Assert.assertEquals(ids(3L, 4L), 
templateManager.getCdromDeviceIds(vm));
+    }
+
+    @Test
+    public void 
getCdromDeviceIdsReturnsDefaultWhenHostDoesNotAdvertiseCdromCap() {
+        UserVmVO vm = mockUserVmOnHostWithCdromCap(null);
+
+        Assert.assertEquals(ids(3L), templateManager.getCdromDeviceIds(vm));
+    }
+
+    @Test
+    public void getCdromDeviceIdsCoversAttachedIsosBeyondHostCap() {
+        UserVmVO vm = mockUserVmOnHostWithCdromCap(2L);
+        Mockito.when(_vmIsoMapDao.listByVmId(1L)).thenReturn(Arrays.asList(
+                new VmIsoMapVO(1L, 11L, 
TemplateManager.CDROM_PRIMARY_DEVICE_SEQ + 1),
+                new VmIsoMapVO(1L, 12L, 
TemplateManager.CDROM_PRIMARY_DEVICE_SEQ + 2)));
+
+        Assert.assertEquals(ids(3L, 4L, 5L), 
templateManager.getCdromDeviceIds(vm));
+    }
+
+    private void mockVmNetworkProvidesConfigDrive(UserVmVO vm, boolean 
providesConfigDrive) {
+        NicVO nic = Mockito.mock(NicVO.class);
+        Mockito.when(nic.getNetworkId()).thenReturn(100L);
+        
Mockito.when(_nicDao.listByVmId(vm.getId())).thenReturn(java.util.Collections.singletonList(nic));
+        
Mockito.lenient().when(_networkModel.isProviderSupportServiceInNetwork(Mockito.eq(100L),
 Mockito.any(Service.class), 
Mockito.eq(Provider.ConfigDrive))).thenReturn(providesConfigDrive);
+    }
+
+    private void withConfiguredMaxCdroms(String value, Runnable test) {
+        Object original = 
org.springframework.test.util.ReflectionTestUtils.getField(TemplateManager.VmIsoMaxCount,
 "_defaultValue");
+        
org.springframework.test.util.ReflectionTestUtils.setField(TemplateManager.VmIsoMaxCount,
 "_defaultValue", value);
+        try {
+            test.run();
+        } finally {
+            
org.springframework.test.util.ReflectionTestUtils.setField(TemplateManager.VmIsoMaxCount,
 "_defaultValue", original);
+        }
+    }
+
+    @Test
+    public void 
isConfigDriveUsedReturnsTrueWhenNetworkProvidesConfigDriveBeforeFirstStart() {
+        UserVmVO vm = mockUserVmOnHostWithCdromCap(null);
+        mockVmNetworkProvidesConfigDrive(vm, true);
+
+        Assert.assertTrue(templateManager.isConfigDriveUsed(vm));
+    }
+
+    @Test
+    public void 
isConfigDriveUsedReturnsTrueWhenConfigDriveLocationDetailExists() {
+        UserVmVO vm = mockUserVmOnHostWithCdromCap(null);
+        Mockito.when(_vmInstanceDetailsDao.findDetail(1L, 
VmDetailConstants.CONFIG_DRIVE_LOCATION)).thenReturn(Mockito.mock(VMInstanceDetailVO.class));
+
+        Assert.assertTrue(templateManager.isConfigDriveUsed(vm));
+    }
+
+    @Test
+    public void 
isConfigDriveUsedReturnsFalseWhenNoNetworkProvidesConfigDrive() {
+        UserVmVO vm = mockUserVmOnHostWithCdromCap(null);
+        mockVmNetworkProvidesConfigDrive(vm, false);
+
+        Assert.assertFalse(templateManager.isConfigDriveUsed(vm));
+    }
+
+    @Test
+    public void getCdromDeviceIdsAddsTheCdromOfConfigDrive() {
+        UserVmVO vm = mockUserVmOnHostWithCdromCap(2L);
+        mockVmNetworkProvidesConfigDrive(vm, true);
+
+        // the config drive ISO is one more CD-ROM than the ones supported for 
the ISOs
+        Assert.assertEquals(ids(3L, 4L, 5L), 
templateManager.getCdromDeviceIds(vm));
+        Assert.assertEquals(ids(3L, 4L), templateManager.getCdromDeviceIds(vm, 
false));
+        Assert.assertEquals(ids(3L, 4L, 5L), 
templateManager.getCdromDeviceIds(vm, true));
+    }
+
+    @Test
+    public void getIsoDeviceSeqKeepsTheDeviceIdOfConfigDrive() {
+        // without config drive, the CD-ROMs of the ISOs are the ones of the 
slots
+        Assert.assertEquals(3, templateManager.getIsoDeviceSeq(3, false));
+        Assert.assertEquals(4, templateManager.getIsoDeviceSeq(4, false));
+        Assert.assertEquals(5, templateManager.getIsoDeviceSeq(5, false));
+        // with config drive (device id 4), the slots from it are moved to the 
next device ids
+        Assert.assertEquals(3, templateManager.getIsoDeviceSeq(3, true));
+        Assert.assertEquals(5, templateManager.getIsoDeviceSeq(4, true));
+        Assert.assertEquals(6, templateManager.getIsoDeviceSeq(5, true));
+    }
+
+    private java.util.List<DiskTO> prepareIsoDisks(UserVmVO vm) {
+        // no ISO is attached to the Instance
+        Mockito.when(vm.getIsoId()).thenReturn(null);
+        VirtualMachineProfile profile = 
Mockito.mock(VirtualMachineProfile.class);
+        Mockito.when(profile.getId()).thenReturn(1L);
+        DeployDestination dest = Mockito.mock(DeployDestination.class);
+        Host host = Mockito.mock(Host.class);
+        Mockito.when(host.getId()).thenReturn(7L);
+        Mockito.when(dest.getHost()).thenReturn(host);
+        
Mockito.when(_hostDao.findById(7L)).thenReturn(Mockito.mock(HostVO.class));
+
+        templateManager.prepareIsoForVmProfile(profile, dest);
+
+        org.mockito.ArgumentCaptor<DiskTO> disks = 
org.mockito.ArgumentCaptor.forClass(DiskTO.class);
+        Mockito.verify(profile, Mockito.atLeast(0)).addDisk(disks.capture());
+        return disks.getAllValues();
+    }
+
+    @Test
+    public void prepareIsoForVmProfileSkipsTheDeviceIdOfConfigDrive() {
+        UserVmVO vm = mockUserVmOnHostWithCdromCap(2L);
+        mockVmNetworkProvidesConfigDrive(vm, true);
+
+        withConfiguredMaxCdroms("2", () -> {
+            java.util.List<DiskTO> disks = prepareIsoDisks(vm);
+
+            // the config drive ISO has the device id 4 (it is added by the 
config drive element), so the second ISO is at 5
+            Assert.assertEquals(Arrays.asList(3L, 5L), 
disks.stream().map(DiskTO::getDiskSeq).collect(java.util.stream.Collectors.toList()));
+        });
+    }
+
+    @Test
+    public void 
prepareIsoForVmProfileUsesConsecutiveDeviceIdsWithoutConfigDrive() {
+        UserVmVO vm = mockUserVmOnHostWithCdromCap(2L);
+        mockVmNetworkProvidesConfigDrive(vm, false);
+
+        withConfiguredMaxCdroms("2", () -> {
+            java.util.List<DiskTO> disks = prepareIsoDisks(vm);
+
+            Assert.assertEquals(Arrays.asList(3L, 4L), 
disks.stream().map(DiskTO::getDiskSeq).collect(java.util.stream.Collectors.toList()));
+        });
+    }
 }
diff --git 
a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
 
b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
index 0e8c67d5127..c5267033dfb 100644
--- 
a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
+++ 
b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
@@ -35,9 +35,11 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
 import org.apache.cloudstack.api.ApiConstants;
@@ -81,6 +83,7 @@ import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import com.cloud.agent.AgentManager;
 import com.cloud.agent.api.Answer;
@@ -2094,4 +2097,25 @@ public class UnmanagedVMsManagerImplTest {
                 Map.of("Network adapter 1", java.util.List.of("10.1.1.151")));
         Assert.assertEquals("10.1.1.151", result.get("Network adapter 
1").getIp4Address());
     }
+
+    @Test
+    public void getReservedDeviceIdsAlwaysReservesTheCdromOfConfigDrive() {
+        VirtualMachine vm = Mockito.mock(VirtualMachine.class);
+        // the networks of the Instance are not imported yet, so the config 
drive is always assumed
+        Mockito.when(volumeApiService.getReservedDeviceIds(vm, 
true)).thenReturn(new HashSet<>(java.util.Arrays.asList(3L, 4L)));
+
+        Set<Long> reserved = 
ReflectionTestUtils.invokeMethod(unmanagedVMsManager, "getReservedDeviceIds", 
vm);
+
+        Assert.assertEquals(new HashSet<>(java.util.Arrays.asList(3L, 4L)), 
reserved);
+    }
+
+    @Test
+    public void getNextDeviceIdSkipsReservedDeviceIds() {
+        Set<Long> reserved = new HashSet<>(java.util.Arrays.asList(3L, 4L));
+
+        Assert.assertEquals(1L, (long) 
ReflectionTestUtils.invokeMethod(unmanagedVMsManager, "getNextDeviceId", 0L, 
reserved));
+        Assert.assertEquals(2L, (long) 
ReflectionTestUtils.invokeMethod(unmanagedVMsManager, "getNextDeviceId", 1L, 
reserved));
+        Assert.assertEquals(5L, (long) 
ReflectionTestUtils.invokeMethod(unmanagedVMsManager, "getNextDeviceId", 2L, 
reserved));
+        Assert.assertEquals(6L, (long) 
ReflectionTestUtils.invokeMethod(unmanagedVMsManager, "getNextDeviceId", 5L, 
reserved));
+    }
 }

Reply via email to