diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 7c3f826be171..f3eb092bb036 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -3833,7 +3833,7 @@ public int compare(final DiskTO arg0, final DiskTO arg1) { DiskDef.DiskBus diskBusTypeData = getDataDiskModelFromVMDetail(vmSpec); if (diskBusTypeData == null) { - diskBusTypeData = (diskBusType == DiskDef.DiskBus.SCSI) ? diskBusType : DiskDef.DiskBus.VIRTIO; + diskBusTypeData = (diskBusType == DiskDef.DiskBus.SCSI || diskBusType == DiskDef.DiskBus.VIRTIOBLK) ? diskBusType : DiskDef.DiskBus.VIRTIO; } final DiskDef disk = new DiskDef(); @@ -4256,13 +4256,15 @@ public synchronized String attachOrDetachDisk(final Connect conn, if (disk.getDeviceType() == DeviceType.DISK) { if (disk.getBusType() == DiskDef.DiskBus.SCSI) { busT = DiskDef.DiskBus.SCSI; + } else if (disk.getBusType() == DiskDef.DiskBus.VIRTIOBLK) { + busT = DiskDef.DiskBus.VIRTIOBLK; } break; } } diskdef = new DiskDef(); - if (busT == DiskDef.DiskBus.SCSI) { + if (busT == DiskDef.DiskBus.SCSI || busT == DiskDef.DiskBus.VIRTIOBLK) { diskdef.setQemuDriver(true); diskdef.setDiscard(DiscardType.UNMAP); } 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 439e4f663416..3f221d749bbf 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 @@ -871,6 +871,17 @@ public enum DiskBus { _bus = bus; } + /** + * The name libvirt knows this bus by. It is the enum's own value everywhere except + * VIRTIOBLK: 'virtio-blk' is a CloudStack disk controller name, not one of libvirt's + * target buses (ide, scsi, virtio, xen, usb, sata, sd, fdc, uml), and a virtio-blk + * disk is a virtio one. Rendering the enum value instead makes libvirt reject the + * domain XML, so the VM cannot start. + */ + public String libvirtBus() { + return this == VIRTIOBLK ? VIRTIO._bus : _bus; + } + public static DiskBus fromValue(String bus) { for (DiskBus b : DiskBus.values()) { if (b.toString().equalsIgnoreCase(bus)) { @@ -1031,7 +1042,7 @@ private String getDevLabel(int devId, DiskBus bus, boolean forIso) { if (bus == DiskBus.SCSI) { return "sd" + getDevLabelSuffix(devId); - } else if (bus == DiskBus.VIRTIO) { + } else if (bus == DiskBus.VIRTIO || bus == DiskBus.VIRTIOBLK) { return "vd" + getDevLabelSuffix(devId); } else if (bus == DiskBus.SATA){ return "sd" + getDevLabelSuffix(devId); @@ -1386,7 +1397,7 @@ public String toString() { diskBuilder.append(String.format("io='%s' ", ioDriver)); } - if (isIothreadsEnabled && _bus == DiskBus.VIRTIO) { + if (isIothreadsEnabled && (_bus == DiskBus.VIRTIO || _bus == DiskBus.VIRTIOBLK)) { diskBuilder.append(String.format("iothread='%s' ", NUMBER_OF_IOTHREADS)); } diskBuilder.append("/>\n"); @@ -1429,7 +1440,7 @@ public String toString() { } diskBuilder.append("\n"); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 0cb32da318ea..0d93471219df 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -1798,6 +1798,17 @@ protected DiskDef.DiskBus getAttachDiskBusType(int deviceId, List disks return DiskDef.DiskBus.VIRTIOBLK; } } + if (deviceId != 0 && DiskDef.DiskBus.VIRTIOBLK == DiskDef.DiskBus.fromValue( + MapUtils.getString(controllerInfo, VmDetailConstants.ROOT_DISK_CONTROLLER))) { + // A data disk with no controller of its own follows the root, as it already does when the + // VM starts. The scan above cannot see this case: a virtio-blk disk is rendered with + // libvirt's 'virtio' bus, so it is indistinguishable from a plain virtio disk in the domain + // XML, and the disk would silently lose discard until the next stop/start. The scan still + // wins when the running VM is on SCSI, so a detail changed after the VM started cannot + // put a virtio disk next to its SCSI ones. + logger.debug("Attaching disk (deviceId={}) with the root controller virtio-blk", deviceId); + return DiskDef.DiskBus.VIRTIOBLK; + } return DiskDef.DiskBus.VIRTIO; } 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 56ad267eac7e..9b52874fddb1 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 @@ -19,6 +19,9 @@ package com.cloud.hypervisor.kvm.resource; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.io.File; import java.util.Arrays; import java.util.List; @@ -597,4 +600,71 @@ public void testTpmModel() { "\n" + "\n", tpmDef.toString()); } + + @Test + public void testDiskDefVirtioBlkUsesVirtioBusAndVdLabel() { + DiskDef disk = new DiskDef(); + disk.defFileBasedDisk("/var/lib/libvirt/images/disk.qcow2", 1, DiskDef.DiskBus.VIRTIOBLK, DiskDef.DiskFmtType.QCOW2); + + String xmlDef = disk.toString(); + // 'virtio-blk' is a CloudStack controller name; libvirt only accepts the bus 'virtio' and + // rejects the whole domain XML otherwise, which is what stopped these VMs from starting. + assertTrue(xmlDef, xmlDef.contains("")); + assertFalse(xmlDef, xmlDef.contains("virtio-blk")); + assertEquals(DiskDef.DiskBus.VIRTIOBLK, disk.getBusType()); + } + + @Test + public void testDiskDefVirtioBlkLabelsMatchVirtio() { + for (int devId = 0; devId < 4; devId++) { + DiskDef virtio = new DiskDef(); + virtio.defFileBasedDisk("/var/lib/libvirt/images/disk.qcow2", devId, DiskDef.DiskBus.VIRTIO, DiskDef.DiskFmtType.QCOW2); + DiskDef virtioBlk = new DiskDef(); + virtioBlk.defFileBasedDisk("/var/lib/libvirt/images/disk.qcow2", devId, DiskDef.DiskBus.VIRTIOBLK, DiskDef.DiskFmtType.QCOW2); + assertEquals(virtio.getDiskLabel(), virtioBlk.getDiskLabel()); + } + } + + @Test + public void testDiskDefVirtioBlkWithDiscardUnmap() { + DiskDef disk = new DiskDef(); + disk.defFileBasedDisk("/var/lib/libvirt/images/disk.qcow2", 0, DiskDef.DiskBus.VIRTIOBLK, DiskDef.DiskFmtType.QCOW2); + disk.setQemuDriver(true); + disk.setDiscard(DiskDef.DiscardType.UNMAP); + + String xmlDef = disk.toString(); + // the point of the virtio-blk controller: a virtio disk that passes the guest's discard through + assertTrue(xmlDef, xmlDef.contains("discard='unmap'")); + assertTrue(xmlDef, xmlDef.contains("bus='virtio'")); + assertFalse(xmlDef, xmlDef.contains("virtio-blk")); + } + + @Test + public void testDiskDefVirtioIsUnchanged() { + DiskDef disk = new DiskDef(); + disk.defFileBasedDisk("/var/lib/libvirt/images/disk.qcow2", 0, DiskDef.DiskBus.VIRTIO, DiskDef.DiskFmtType.QCOW2); + + String xmlDef = disk.toString(); + assertTrue(xmlDef, xmlDef.contains("")); + assertFalse(xmlDef, xmlDef.contains("discard=")); + } + + @Test + public void testDiskDefScsiIsUnchanged() { + DiskDef disk = new DiskDef(); + disk.defFileBasedDisk("/var/lib/libvirt/images/disk.qcow2", 0, DiskDef.DiskBus.SCSI, DiskDef.DiskFmtType.QCOW2); + + String xmlDef = disk.toString(); + assertTrue(xmlDef, xmlDef.contains("")); + } + + @Test + public void testDiskDefVirtioBlkGetsAnIothreadLikeVirtio() { + DiskDef disk = new DiskDef(); + disk.defFileBasedDisk("/var/lib/libvirt/images/disk.qcow2", 0, DiskDef.DiskBus.VIRTIOBLK, DiskDef.DiskFmtType.QCOW2); + disk.isIothreadsEnabled(true); + + String xmlDef = disk.toString(); + assertTrue(xmlDef, xmlDef.contains("iothread=")); + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java index a85fc31224e0..e3aa13193789 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java @@ -23,6 +23,7 @@ import com.ceph.rbd.Rbd; import com.ceph.rbd.RbdException; import com.ceph.rbd.RbdImage; +import com.cloud.vm.VmDetailConstants; import com.cloud.exception.InternalErrorException; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; @@ -765,4 +766,48 @@ public void ensureLibvirtSupportsEncryptedRbdHotplugIgnoresNonRbd() { // version is not even consulted for them. storageProcessor.ensureLibvirtSupportsEncryptedRbdHotplug(StoragePoolType.NetworkFilesystem); } + + private LibvirtVMDef.DiskDef diskWithBus(LibvirtVMDef.DiskDef.DiskBus bus) { + LibvirtVMDef.DiskDef disk = new LibvirtVMDef.DiskDef(); + disk.defFileBasedDisk("/var/lib/libvirt/images/disk.qcow2", 0, bus, LibvirtVMDef.DiskDef.DiskFmtType.QCOW2); + return disk; + } + + @Test + public void getAttachDiskBusTypeUsesTheDataDiskControllerWhenItIsSet() { + Map controllerInfo = Map.of(VmDetailConstants.DATA_DISK_CONTROLLER, "virtio-blk"); + Assert.assertEquals(LibvirtVMDef.DiskDef.DiskBus.VIRTIOBLK, + storageProcessor.getAttachDiskBusType(1, List.of(diskWithBus(LibvirtVMDef.DiskDef.DiskBus.VIRTIO)), controllerInfo)); + } + + @Test + public void getAttachDiskBusTypeFollowsAVirtioBlkRootForDataDisks() { + // a virtio-blk disk renders with libvirt's 'virtio' bus, so scanning the domain XML cannot + // tell the two apart; without this the hot-plugged disk loses discard until the next start + Map controllerInfo = Map.of(VmDetailConstants.ROOT_DISK_CONTROLLER, "virtio-blk"); + Assert.assertEquals(LibvirtVMDef.DiskDef.DiskBus.VIRTIOBLK, + storageProcessor.getAttachDiskBusType(1, List.of(diskWithBus(LibvirtVMDef.DiskDef.DiskBus.VIRTIO)), controllerInfo)); + } + + @Test + public void getAttachDiskBusTypeKeepsVirtioForAVirtioRoot() { + Map controllerInfo = Map.of(VmDetailConstants.ROOT_DISK_CONTROLLER, "virtio"); + Assert.assertEquals(LibvirtVMDef.DiskDef.DiskBus.VIRTIO, + storageProcessor.getAttachDiskBusType(1, List.of(diskWithBus(LibvirtVMDef.DiskDef.DiskBus.VIRTIO)), controllerInfo)); + } + + @Test + public void getAttachDiskBusTypeStillDetectsScsiFromTheRunningDisks() { + Assert.assertEquals(LibvirtVMDef.DiskDef.DiskBus.SCSI, + storageProcessor.getAttachDiskBusType(1, List.of(diskWithBus(LibvirtVMDef.DiskDef.DiskBus.SCSI)), Map.of())); + } + + @Test + public void getAttachDiskBusTypePrefersTheRunningScsiDisksOverAVirtioBlkRootDetail() { + // the detail can be changed on a running VM; the disks it actually runs on win, so a SCSI VM + // does not get a virtio disk hot-plugged next to its sd* ones + Map controllerInfo = Map.of(VmDetailConstants.ROOT_DISK_CONTROLLER, "virtio-blk"); + Assert.assertEquals(LibvirtVMDef.DiskDef.DiskBus.SCSI, + storageProcessor.getAttachDiskBusType(1, List.of(diskWithBus(LibvirtVMDef.DiskDef.DiskBus.SCSI)), controllerInfo)); + } }