runtime: plumb block discard unmap

Pass block-device discard support through the Go QEMU stack.

Block drives can now carry a DiscardUnmap request into govmm. QEMU
command-line and QMP hotplug paths set discard=unmap on the backend and
enable discard on virtio-blk frontends, while leaving SCSI frontend
arguments unchanged.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
Assisted-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Manuel Huber
2026-06-03 18:05:24 +00:00
parent 3332b7c4bc
commit c9352ffffe
7 changed files with 104 additions and 6 deletions

View File

@@ -1378,6 +1378,9 @@ type BlockDevice struct {
// ReadOnly sets the block device in readonly mode
ReadOnly bool
// DiscardUnmap enables discard/unmap support for this block device.
DiscardUnmap bool
// Transport is the virtio transport for this device.
Transport VirtioTransport
}
@@ -1425,6 +1428,9 @@ func (blkdev BlockDevice) QemuParams(config *Config) []string {
if blkdev.ShareRW {
deviceParams = append(deviceParams, "share-rw=on")
}
if blkdev.DiscardUnmap {
deviceParams = append(deviceParams, "discard=on")
}
deviceParams = append(deviceParams, fmt.Sprintf("serial=%s", blkdev.ID))
@@ -1437,6 +1443,9 @@ func (blkdev BlockDevice) QemuParams(config *Config) []string {
if blkdev.ReadOnly {
blkParams = append(blkParams, "readonly=on")
}
if blkdev.DiscardUnmap {
blkParams = append(blkParams, "discard=unmap")
}
qemuParams = append(qemuParams, "-device")
qemuParams = append(qemuParams, strings.Join(deviceParams, ","))

View File

@@ -342,6 +342,29 @@ func TestAppendDeviceBlock(t *testing.T) {
testAppend(blkdev, deviceBlockString, t)
}
func TestAppendDeviceBlockDiscardUnmap(t *testing.T) {
blkdev := BlockDevice{
Driver: VirtioBlock,
ID: "hd0",
File: "/var/lib/vm.img",
AIO: Threads,
Format: QCOW2,
Interface: NoInterface,
WCE: false,
DisableModern: true,
ROMFile: romfile,
ShareRW: true,
ReadOnly: true,
DiscardUnmap: true,
}
params := strings.Join(blkdev.QemuParams(&Config{}), " ")
for _, opt := range []string{"discard=on", "discard=unmap"} {
if !strings.Contains(params, opt) {
t.Fatalf("missing %s in block device params: %s", opt, params)
}
}
}
func TestAppendDeviceVFIO(t *testing.T) {
vfioDevice := VFIODevice{
BDF: "02:10.0",

View File

@@ -800,6 +800,10 @@ func (q *QMP) blockdevAddBaseArgs(driver string, blockDevice *BlockDevice) map[s
}
blockdevArgs["node-name"] = blockDevice.ID
if blockDevice.DiscardUnmap {
blockdevArgs["discard"] = "unmap"
blockdevArgs["file"].(map[string]interface{})["discard"] = "unmap"
}
return blockdevArgs
}
@@ -863,6 +867,16 @@ func (q *QMP) ExecuteBlockdevAddWithDriverCache(ctx context.Context, driver stri
// the logical and physical block sizes for the device; if either is 0, the
// hypervisor default is used for that size.
func (q *QMP) ExecuteDeviceAdd(ctx context.Context, blockdevID, devID, driver, bus, romfile string, shared, disableModern bool, logicalBlockSize, physicalBlockSize uint32) error {
return q.executeDeviceAdd(ctx, blockdevID, devID, driver, bus, romfile, shared, disableModern, false, logicalBlockSize, physicalBlockSize)
}
// ExecuteDeviceAddWithDiscard is like ExecuteDeviceAdd, with explicit virtio-blk
// discard support.
func (q *QMP) ExecuteDeviceAddWithDiscard(ctx context.Context, blockdevID, devID, driver, bus, romfile string, shared, disableModern, discardUnmap bool, logicalBlockSize, physicalBlockSize uint32) error {
return q.executeDeviceAdd(ctx, blockdevID, devID, driver, bus, romfile, shared, disableModern, discardUnmap, logicalBlockSize, physicalBlockSize)
}
func (q *QMP) executeDeviceAdd(ctx context.Context, blockdevID, devID, driver, bus, romfile string, shared, disableModern, discardUnmap bool, logicalBlockSize, physicalBlockSize uint32) error {
args := map[string]interface{}{
"id": devID,
"driver": driver,
@@ -880,6 +894,9 @@ func (q *QMP) ExecuteDeviceAdd(ctx context.Context, blockdevID, devID, driver, b
if shared {
args["share-rw"] = true
}
if discardUnmap && strings.HasPrefix(driver, "virtio-blk") {
args["discard"] = true
}
if transport.isVirtioPCI(nil) {
args["romfile"] = romfile
@@ -1121,6 +1138,16 @@ func (q *QMP) ExecuteDeviceDel(ctx context.Context, devID string) error {
// 1.0 in nested environments. logicalBlockSize and physicalBlockSize specify the logical and
// physical sector sizes reported to the guest; set to 0 to use the hypervisor default.
func (q *QMP) ExecutePCIDeviceAdd(ctx context.Context, blockdevID, devID, driver, addr, bus, romfile string, queues int, shared, disableModern bool, iothreadID string, logicalBlockSize, physicalBlockSize uint32) error {
return q.executePCIDeviceAdd(ctx, blockdevID, devID, driver, addr, bus, romfile, queues, shared, disableModern, false, iothreadID, logicalBlockSize, physicalBlockSize)
}
// ExecutePCIDeviceAddWithDiscard is like ExecutePCIDeviceAdd, with explicit
// virtio-blk discard support.
func (q *QMP) ExecutePCIDeviceAddWithDiscard(ctx context.Context, blockdevID, devID, driver, addr, bus, romfile string, queues int, shared, disableModern, discardUnmap bool, iothreadID string, logicalBlockSize, physicalBlockSize uint32) error {
return q.executePCIDeviceAdd(ctx, blockdevID, devID, driver, addr, bus, romfile, queues, shared, disableModern, discardUnmap, iothreadID, logicalBlockSize, physicalBlockSize)
}
func (q *QMP) executePCIDeviceAdd(ctx context.Context, blockdevID, devID, driver, addr, bus, romfile string, queues int, shared, disableModern, discardUnmap bool, iothreadID string, logicalBlockSize, physicalBlockSize uint32) error {
args := map[string]interface{}{
"id": devID,
"driver": driver,
@@ -1133,6 +1160,9 @@ func (q *QMP) ExecutePCIDeviceAdd(ctx context.Context, blockdevID, devID, driver
if shared {
args["share-rw"] = true
}
if discardUnmap && strings.HasPrefix(driver, "virtio-blk") {
args["discard"] = true
}
if queues > 0 {
args["num-queues"] = queues
}

View File

@@ -508,6 +508,25 @@ func TestQMPBlockdevAddWithCache(t *testing.T) {
<-disconnectedCh
}
func TestQMPBlockdevAddDiscardUnmapArgs(t *testing.T) {
q := &QMP{}
dev := BlockDevice{
ID: fmt.Sprintf("drive_%s", volumeUUID),
File: "/tmp/disk.img",
AIO: Native,
DiscardUnmap: true,
}
args := q.blockdevAddBaseArgs("file", &dev)
if got := args["discard"]; got != "unmap" {
t.Fatalf("unexpected discard option: got %v, expecting unmap", got)
}
fileArgs := args["file"].(map[string]interface{})
if got := fileArgs["discard"]; got != "unmap" {
t.Fatalf("unexpected file discard option: got %v, expecting unmap", got)
}
}
// Checks that the netdev_add command is correctly sent.
//
// We start a QMPLoop, send the netdev_add command and stop the loop.

View File

@@ -2097,10 +2097,11 @@ func (q *qemu) hotplugAddBlockDevice(ctx context.Context, drive *config.BlockDri
}
qblkDevice := govmmQemu.BlockDevice{
ID: drive.ID,
File: drive.File,
ReadOnly: drive.ReadOnly,
AIO: govmmQemu.BlockDeviceAIO(q.config.BlockDeviceAIO),
ID: drive.ID,
File: drive.File,
ReadOnly: drive.ReadOnly,
DiscardUnmap: drive.DiscardUnmap,
AIO: govmmQemu.BlockDeviceAIO(q.config.BlockDeviceAIO),
}
if drive.Swap {
@@ -2157,7 +2158,7 @@ func (q *qemu) hotplugAddBlockDevice(ctx context.Context, drive *config.BlockDri
iothreadID = fmt.Sprintf("%s_%d", indepIOThreadsPrefix, 0)
}
if err = q.qmpMonitorCh.qmp.ExecutePCIDeviceAdd(q.qmpMonitorCh.ctx, drive.ID, devID, driver, addr, bridge.ID, romFile, queues, true, defaultDisableModern, iothreadID, q.config.BlockDeviceLogicalSectorSize, q.config.BlockDevicePhysicalSectorSize); err != nil {
if err = q.qmpMonitorCh.qmp.ExecutePCIDeviceAddWithDiscard(q.qmpMonitorCh.ctx, drive.ID, devID, driver, addr, bridge.ID, romFile, queues, true, defaultDisableModern, drive.DiscardUnmap, iothreadID, q.config.BlockDeviceLogicalSectorSize, q.config.BlockDevicePhysicalSectorSize); err != nil {
return err
}
case q.config.BlockDeviceDriver == config.VirtioBlockCCW:
@@ -2176,7 +2177,7 @@ func (q *qemu) hotplugAddBlockDevice(ctx context.Context, drive *config.BlockDri
if err != nil {
return err
}
if err = q.qmpMonitorCh.qmp.ExecuteDeviceAdd(q.qmpMonitorCh.ctx, drive.ID, devID, driver, devNoHotplug, "", true, false, q.config.BlockDeviceLogicalSectorSize, q.config.BlockDevicePhysicalSectorSize); err != nil {
if err = q.qmpMonitorCh.qmp.ExecuteDeviceAddWithDiscard(q.qmpMonitorCh.ctx, drive.ID, devID, driver, devNoHotplug, "", true, false, drive.DiscardUnmap, q.config.BlockDeviceLogicalSectorSize, q.config.BlockDevicePhysicalSectorSize); err != nil {
return err
}
case q.config.BlockDeviceDriver == config.VirtioSCSI:

View File

@@ -692,6 +692,7 @@ func genericBlockDevice(drive config.BlockDrive, nestedRun bool) (govmmQemu.Bloc
DisableModern: nestedRun,
ShareRW: drive.ShareRW,
ReadOnly: drive.ReadOnly,
DiscardUnmap: drive.DiscardUnmap,
}, nil
}

View File

@@ -526,6 +526,21 @@ func TestQemuArchBaseAppendBlockDevice(t *testing.T) {
testQemuArchBaseAppend(t, drive, expectedOut)
}
func TestGenericBlockDeviceDiscardUnmap(t *testing.T) {
assert := assert.New(t)
drive := config.BlockDrive{
File: "/root",
Format: "raw",
ID: "blockDevTest",
DiscardUnmap: true,
}
blockDevice, err := genericBlockDevice(drive, false)
assert.NoError(err)
assert.True(blockDevice.DiscardUnmap)
}
func TestQemuArchBaseAppendVhostUserDevice(t *testing.T) {
socketPath := "nonexistentpath.sock"
macAddress := "00:11:22:33:44:55:66"