incus-mirror/internal/server/instance/drivers/qmp/commands.go
Stéphane Graber d4752a6ce0
incusd/instance/qmp: Add copy-before-write export helpers
Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
2026-07-28 22:53:30 -04:00

2010 lines
47 KiB
Go

package qmp
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/revert"
)
// ChardevChangeInfo contains information required to change the backend of a chardev.
type ChardevChangeInfo struct {
Type string `json:"type"`
File *os.File `json:"file,omitempty"`
FDName string `json:"fdname,omitempty"`
}
// FdsetFdInfo contains information about a file descriptor that belongs to an FD set.
type FdsetFdInfo struct {
FD int `json:"fd"`
Opaque string `json:"opaque"`
}
// FdsetInfo contains information about an FD set.
type FdsetInfo struct {
ID int `json:"fdset-id"`
FDs []FdsetFdInfo `json:"fds"`
}
// AddFdInfo contains information about a file descriptor that was added to an fd set.
type AddFdInfo struct {
ID int `json:"fdset-id"`
FD int `json:"fd"`
}
// CPUInstanceProperties contains CPU instance properties.
type CPUInstanceProperties struct {
NodeID int `json:"node-id,omitempty"`
SocketID int `json:"socket-id,omitempty"`
DieID int `json:"die-id,omitempty"`
ClusterID int `json:"cluster-id,omitempty"`
CoreID int `json:"core-id,omitempty"`
ThreadID int `json:"thread-id,omitempty"`
}
// CPU contains information about a CPU.
type CPU struct {
Index int `json:"cpu-index,omitempty"`
QOMPath string `json:"qom-path,omitempty"`
ThreadID int `json:"thread-id,omitempty"`
Target string `json:"target,omitempty"`
Props CPUInstanceProperties `json:"props"`
}
// HotpluggableCPU contains information about a hotpluggable CPU.
type HotpluggableCPU struct {
Type string `json:"type"`
VCPUsCount int `json:"vcpus-count"`
QOMPath string `json:"qom-path,omitempty"`
Props CPUInstanceProperties `json:"props"`
}
// CPUModel contains information about a CPU model.
type CPUModel struct {
Name string `json:"name"`
Flags map[string]any `json:"props"`
}
// MemoryDevice contains information about a memory device.
type MemoryDevice struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
// PCDimmDevice contains information about a memory device of type pc-dimm.
type PCDimmDevice struct {
ID string `json:"id"`
Addr uint64 `json:"addr"`
Slot int `json:"slot"`
Node int `json:"node"`
Memdev string `json:"memdev"`
Hotpluggable bool `json:"hotpluggable"`
Hotplugged bool `json:"hotplugged"`
}
// MemDev contains information about a memory device.
type MemDev struct {
ID string `json:"id"`
Size int `json:"size"`
Merge bool `json:"merge"`
Dump bool `json:"dump"`
Prealloc bool `json:"prealloc"`
Share bool `json:"share"`
Reserve bool `json:"reserve"`
HostNodes []int `json:"host-nodes"`
}
// MigrationStatus contains information about the ongoing migration.
type MigrationStatus struct {
Status string `json:"status"`
RAM struct {
Transferred int64 `json:"transferred"`
Remaining int64 `json:"remaining"`
Total int64 `json:"total"`
Duplicate int64 `json:"duplicate"`
Normal int64 `json:"normal"`
NormalBytes int64 `json:"normal-bytes"`
DirtyPagesRate int64 `json:"diry-pages-rate"`
MBps float64 `json:"mbps"`
DirtySyncCount int64 `json:"diry-sync-count"`
PostcopyRequests int64 `json:"postcopy-requests"`
PageSize int64 `json:"page-size"`
MultiFDBytes int64 `json:"multifd-bytes"`
PagesPerSecond int64 `json:"pages-per-second"`
PrecopyBytes int64 `json:"precopy-bytes"`
DowntimeBytes int64 `json:"downtime-bytes"`
PostcopyBytes int64 `json:"postcopy-bytes"`
DirtySyncMissedZeroCopy int64 `json:"diry-sync-missed-zero-copy"`
} `json:"ram"`
TotalTime int64 `json:"total-time"`
DownTime int64 `json:"down-time"`
ExpectedDowntime int64 `json:"expected-downtime"`
SetupTime int64 `json:"setup-time"`
CPUThrottlePercentage int64 `json:"cpu-throttle-percentage"`
PostcopyBlocktime int64 `json:"postcopy-blocktime"`
PostcopyVCPUBlocktime []int64 `json:"postcopy-vcpu-blocktime"`
DirtyLimitThrottleTimePerRound int64 `json:"dirty-limit-throttle-time-per-round"`
DirtyLimitRingFullTime int64 `json:"dirty-limit-ring-full-time"`
}
// BlockExport contains information about the exported block.
type BlockExport struct {
NodeName string `json:"node-name"`
Type string `json:"type"`
}
// BlockDirtyInfo contains dirty bitmap information.
type BlockDirtyInfo struct {
Name string `json:"name"`
Count int `json:"count"`
Granularity int `json:"granularity"`
Recording bool `json:"recording"`
Busy bool `json:"busy"`
Persistent bool `json:"persistent"`
Inconsistent bool `json:"inconsistent"`
}
// BlockDeviceInfo contains information about the backing device for a block device.
type BlockDeviceInfo struct {
NodeName string `json:"node-name"`
DirtyBitmaps []BlockDirtyInfo `json:"dirty-bitmaps"`
}
// BlockInfo contains information about a virtual block device.
type BlockInfo struct {
Device string `json:"device"`
Inserted BlockDeviceInfo `json:"inserted"`
}
// QueryCPUs returns a list of CPUs.
func (m *Monitor) QueryCPUs() ([]CPU, error) {
// Prepare the response.
var resp struct {
Return []CPU `json:"return"`
}
err := m.Run("query-cpus-fast", nil, &resp)
if err != nil {
return nil, fmt.Errorf("Failed to query CPUs: %w", err)
}
return resp.Return, nil
}
// QueryHotpluggableCPUs returns a list of hotpluggable CPUs.
func (m *Monitor) QueryHotpluggableCPUs() ([]HotpluggableCPU, error) {
// Prepare the response.
var resp struct {
Return []HotpluggableCPU `json:"return"`
}
err := m.Run("query-hotpluggable-cpus", nil, &resp)
if err != nil {
return nil, fmt.Errorf("Failed to query hotpluggable CPUs: %w", err)
}
return resp.Return, nil
}
// QueryCPUModel returns a CPUModel for the specified model name.
func (m *Monitor) QueryCPUModel(model string) (*CPUModel, error) {
// Prepare the response.
var resp struct {
Return struct {
Model CPUModel `json:"model"`
} `json:"return"`
}
args := map[string]any{
"model": map[string]string{"name": model},
"type": "full",
}
err := m.Run("query-cpu-model-expansion", args, &resp)
if err != nil {
return nil, fmt.Errorf("Failed to query CPU model: %w", err)
}
return &resp.Return.Model, nil
}
// Status returns the current VM status.
func (m *Monitor) Status() (string, error) {
// Prepare the response.
var resp struct {
Return struct {
Status string `json:"status"`
} `json:"return"`
}
// Query the status.
err := m.Run("query-status", nil, &resp)
if err != nil {
return "", err
}
return resp.Return.Status, nil
}
// MachineDefinition returns the current QEMU machine definition name.
func (m *Monitor) MachineDefinition() (string, error) {
// Prepare the request.
var req struct {
Path string `json:"path"`
Property string `json:"property"`
}
req.Path = "/machine"
req.Property = "type"
// Prepare the response.
var resp struct {
Return string `json:"return"`
}
// Query the machine.
err := m.Run("qom-get", req, &resp)
if err != nil {
return "", err
}
return strings.TrimSuffix(resp.Return, "-machine"), nil
}
// MemoryConfiguration returns the current QEMU machine memory configuration (current, max, slots).
func (m *Monitor) MemoryConfiguration() (int64, int64, int64, error) {
// Prepare the request.
var req struct {
Path string `json:"path"`
Property string `json:"property"`
}
req.Path = "/machine"
req.Property = "memory"
// Prepare the response.
var resp struct {
Return struct {
Size int64 `json:"size"`
Slots int64 `json:"slots"`
MaxSize int64 `json:"max-size"`
} `json:"return"`
}
// Query the machine.
err := m.Run("qom-get", req, &resp)
if err != nil {
return -1, -1, -1, err
}
return resp.Return.Size, resp.Return.MaxSize, resp.Return.Slots, nil
}
// SendFile adds a new file descriptor to the QMP fd table associated to name.
func (m *Monitor) SendFile(name string, file *os.File) error {
// Check if disconnected.
if m.disconnected {
return ErrMonitorDisconnect
}
id := m.qmp.qmpIncreaseID()
req := &qmpCommand{
ID: id,
Execute: "getfd",
Arguments: map[string]any{"fdname": name},
}
reqJSON, err := json.Marshal(req)
if err != nil {
return err
}
// Query the status.
_, err = m.qmp.runWithFile(reqJSON, file, id)
if err != nil {
if errors.Is(err, ErrMonitorTimeout) {
return err
}
// Confirm the daemon didn't die.
errPing := m.ping()
if errPing != nil {
return errPing
}
return err
}
return nil
}
// CloseFile closes an existing file descriptor in the QMP fd table associated to name.
func (m *Monitor) CloseFile(name string) error {
var req struct {
FDName string `json:"fdname"`
}
req.FDName = name
err := m.Run("closefd", req, nil)
if err != nil {
return err
}
return nil
}
// SendFileWithFDSet adds a new file descriptor to an FD set.
func (m *Monitor) SendFileWithFDSet(name string, file *os.File, readonly bool) (*AddFdInfo, error) {
// Check if disconnected.
if m.disconnected {
return nil, ErrMonitorDisconnect
}
permissions := "rdwr"
if readonly {
permissions = "rdonly"
}
id := m.qmp.qmpIncreaseID()
req := &qmpCommand{
ID: id,
Execute: "add-fd",
Arguments: map[string]any{
"opaque": fmt.Sprintf("%s:%s", permissions, name),
},
}
reqJSON, err := json.Marshal(req)
if err != nil {
return nil, err
}
ret, err := m.qmp.runWithFile(reqJSON, file, id)
if err != nil {
if errors.Is(err, ErrMonitorTimeout) {
return nil, err
}
// Confirm the daemon didn't die.
errPing := m.ping()
if errPing != nil {
return nil, errPing
}
return nil, err
}
// Prepare the response.
var resp struct {
Return AddFdInfo `json:"return"`
}
err = json.Unmarshal(ret, &resp)
if err != nil {
return nil, err
}
return &resp.Return, nil
}
// RemoveFDFromFDSet removes an FD with the given name from an FD set.
func (m *Monitor) RemoveFDFromFDSet(name string) error {
// Prepare the response.
var resp struct {
Return []FdsetInfo `json:"return"`
}
err := m.Run("query-fdsets", nil, &resp)
if err != nil {
return fmt.Errorf("Failed to query fd sets: %w", err)
}
for _, fdSet := range resp.Return {
for _, fd := range fdSet.FDs {
fields := strings.SplitN(fd.Opaque, ":", 2)
opaque := ""
if len(fields) == 2 {
opaque = fields[1]
} else {
opaque = fields[0]
}
if opaque == name {
args := map[string]any{
"fdset-id": fdSet.ID,
}
err = m.Run("remove-fd", args, nil)
if err != nil {
return fmt.Errorf("Failed to remove fd from fd set: %w", err)
}
}
}
}
return nil
}
// MigrateSetCapabilities sets the capabilities used during migration.
func (m *Monitor) MigrateSetCapabilities(caps map[string]bool) error {
var args struct {
Capabilities []struct {
Capability string `json:"capability"`
State bool `json:"state"`
} `json:"capabilities"`
}
for capName, state := range caps {
args.Capabilities = append(args.Capabilities, struct {
Capability string `json:"capability"`
State bool `json:"state"`
}{
Capability: capName,
State: state,
})
}
err := m.Run("migrate-set-capabilities", args, nil)
if err != nil {
return err
}
return nil
}
// MigrateSetParameters sets the parameters used during migration.
func (m *Monitor) MigrateSetParameters(parameters map[string]any) error {
err := m.Run("migrate-set-parameters", parameters, nil)
if err != nil {
return err
}
return nil
}
// Migrate starts a migration stream.
func (m *Monitor) Migrate(name string) error {
// Query the status.
type migrateArgsChannel struct {
ChannelType string `json:"channel-type"`
Address map[string]string `json:"addr"`
}
type migrateArgs struct {
Channels []migrateArgsChannel `json:"channels"`
}
args := migrateArgs{}
args.Channels = []migrateArgsChannel{{
ChannelType: "main",
Address: map[string]string{
"transport": "socket",
"type": "fd",
"str": name,
},
}}
err := m.Run("migrate", args, nil)
if err != nil {
return err
}
return nil
}
// QueryMigrate gets the current migration status.
func (m *Monitor) QueryMigrate() (*MigrationStatus, error) {
var resp struct {
Return MigrationStatus `json:"return"`
}
err := m.Run("query-migrate", nil, &resp)
if err != nil {
return nil, err
}
return &resp.Return, nil
}
// MigrateWait waits until migration job reaches the specified status.
// Returns nil if the migraton job reaches the specified status or an error if the migration job is in the failed
// status.
func (m *Monitor) MigrateWait(ctx context.Context, state string) error {
// Wait until it completes or fails.
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Prepare the response.
var resp struct {
Return struct {
Status string `json:"status"`
} `json:"return"`
}
err := m.Run("query-migrate", nil, &resp)
if err != nil {
return err
}
if resp.Return.Status == "failed" {
return errors.New("Migrate call failed")
}
if resp.Return.Status == state {
return nil
}
time.Sleep(1 * time.Second)
}
}
}
// MigrateContinue continues a migration stream.
func (m *Monitor) MigrateContinue(fromState string) error {
var args struct {
State string `json:"state"`
}
args.State = fromState
err := m.Run("migrate-continue", args, nil)
if err != nil {
return err
}
return nil
}
// MigrateIncoming starts the receiver of a migration stream.
func (m *Monitor) MigrateIncoming(ctx context.Context, name string) error {
type migrateArgsChannel struct {
ChannelType string `json:"channel-type"`
Address map[string]string `json:"addr"`
}
type migrateArgs struct {
Channels []migrateArgsChannel `json:"channels"`
}
args := migrateArgs{}
args.Channels = []migrateArgsChannel{{
ChannelType: "main",
Address: map[string]string{
"transport": "socket",
"type": "fd",
"str": name,
},
}}
// Query the status.
err := m.Run("migrate-incoming", args, nil)
if err != nil {
return err
}
// Wait until it completes or fails.
for {
// Prepare the response.
var resp struct {
Return struct {
Status string `json:"status"`
} `json:"return"`
}
err := m.Run("query-migrate", nil, &resp)
if err != nil {
return err
}
if resp.Return.Status == "failed" {
return errors.New("Migrate incoming call failed")
}
if resp.Return.Status == "completed" {
return nil
}
// Check context is cancelled last after checking job status.
// This way if the context is cancelled when the migration stream is ended this gives a chance to
// check for job success/failure before checking if the context has been cancelled.
err = ctx.Err()
if err != nil {
return err
}
time.Sleep(1 * time.Second)
}
}
// Powerdown tells the VM to gracefully shutdown.
func (m *Monitor) Powerdown() error {
return m.Run("system_powerdown", nil, nil)
}
// Start tells QEMU to start the emulation.
func (m *Monitor) Start() error {
return m.Run("cont", nil, nil)
}
// Pause tells QEMU to temporarily stop the emulation.
func (m *Monitor) Pause() error {
return m.Run("stop", nil, nil)
}
// Quit tells QEMU to exit immediately.
func (m *Monitor) Quit() error {
return m.Run("quit", nil, nil)
}
// GetCPUs fetches the vCPU information for pinning.
func (m *Monitor) GetCPUs() ([]int, error) {
// Prepare the response.
var resp struct {
Return []struct {
CPU int `json:"cpu-index"`
PID int `json:"thread-id"`
} `json:"return"`
}
// Query the consoles.
err := m.Run("query-cpus-fast", nil, &resp)
if err != nil {
return nil, err
}
// Make a slice of PIDs.
pids := []int{}
for _, cpu := range resp.Return {
pids = append(pids, cpu.PID)
}
return pids, nil
}
// GetMemorySizeBytes returns the current size of the base memory in bytes.
func (m *Monitor) GetMemorySizeBytes() (int64, error) {
// Prepare the response.
var resp struct {
Return struct {
BaseMemory int64 `json:"base-memory"`
} `json:"return"`
}
err := m.Run("query-memory-size-summary", nil, &resp)
if err != nil {
return -1, err
}
return resp.Return.BaseMemory, nil
}
// GetMemoryBalloonSizeBytes returns effective size of the memory in bytes (considering the current balloon size).
func (m *Monitor) GetMemoryBalloonSizeBytes() (int64, error) {
// Prepare the response.
var resp struct {
Return struct {
Actual int64 `json:"actual"`
} `json:"return"`
}
err := m.Run("query-balloon", nil, &resp)
if err != nil {
return -1, err
}
return resp.Return.Actual, nil
}
// SetMemoryBalloonSizeBytes sets the size of the memory in bytes (which will resize the balloon as needed).
func (m *Monitor) SetMemoryBalloonSizeBytes(sizeBytes int64) error {
args := map[string]int64{"value": sizeBytes}
return m.Run("balloon", args, nil)
}
// GetMemdev retrieves memory devices by executing the query-memdev QMP command.
func (m *Monitor) GetMemdev() ([]MemDev, error) {
// Prepare the response.
var resp struct {
Return []MemDev `json:"return"`
}
err := m.Run("query-memdev", nil, &resp)
if err != nil {
return nil, err
}
return resp.Return, nil
}
// GetMemoryDevices retrieves memory devices by executing the query-memory-devices QMP command.
func (m *Monitor) GetMemoryDevices() ([]MemoryDevice, error) {
// Prepare the response.
var resp struct {
Return []MemoryDevice `json:"return"`
}
err := m.Run("query-memory-devices", nil, &resp)
if err != nil {
return nil, err
}
return resp.Return, nil
}
// GetDimmDevices returns a list of memory devices of type pc-dimm.
func (m *Monitor) GetDimmDevices() ([]PCDimmDevice, error) {
devices, err := m.GetMemoryDevices()
if err != nil {
return nil, err
}
result := []PCDimmDevice{}
for _, dev := range devices {
if dev.Type != "dimm" {
continue
}
var dimmData PCDimmDevice
err := json.Unmarshal(dev.Data, &dimmData)
if err != nil {
continue
}
result = append(result, dimmData)
}
return result, nil
}
// AddObject adds a new object.
func (m *Monitor) AddObject(args map[string]any) error {
err := m.Run("object-add", &args, nil)
if err != nil {
return fmt.Errorf("Failed adding object: %w", err)
}
return nil
}
// AddBlockDevice adds a block device.
func (m *Monitor) AddBlockDevice(blockDev map[string]any, device map[string]any, usb bool) error {
reverter := revert.New()
defer reverter.Fail()
// If USB, start with a USB block only controller.
var usbID string
if usb {
id, ok := device["id"].(string)
if !ok {
return errors.New("Parent device must have an id")
}
usbID = "usb_" + id
usbDev := map[string]any{
"driver": "usb-bot",
"bus": "qemu_usb.0",
"id": usbID,
}
err := m.AddDevice(usbDev)
if err != nil {
return fmt.Errorf("Failed adding USB device: %w", err)
}
reverter.Add(func() {
_ = m.RemoveDevice(usbID)
})
device["bus"] = usbID + ".0"
}
nodeName, ok := blockDev["node-name"].(string)
if !ok {
return errors.New("Device node name must be a string")
}
if blockDev != nil {
err := m.Run("blockdev-add", blockDev, nil)
if err != nil {
return fmt.Errorf("Failed adding block device: %w", err)
}
reverter.Add(func() {
_ = m.RemoveBlockDevice(nodeName)
})
}
err := m.AddDevice(device)
if err != nil {
return fmt.Errorf("Failed adding device: %w", err)
}
// If USB, bring the device up now.
if usb {
err = m.Run("qom-set", map[string]any{"path": usbID, "property": "attached", "value": true}, nil)
if err != nil {
return fmt.Errorf("Failed to bring up USB device: %w", err)
}
}
reverter.Success()
return nil
}
// RemoveBlockDevice removes a block device.
func (m *Monitor) RemoveBlockDevice(blockDevName string) error {
if blockDevName != "" {
blockDevName := map[string]string{
"node-name": blockDevName,
}
err := m.Run("blockdev-del", blockDevName, nil)
if err != nil {
if strings.Contains(err.Error(), "is in use") {
return api.StatusErrorf(http.StatusLocked, "%s", err.Error())
}
if strings.Contains(err.Error(), "Failed to find") {
return nil
}
return fmt.Errorf("Failed removing block device: %w", err)
}
}
return nil
}
// AddCharDevice adds a new character device.
func (m *Monitor) AddCharDevice(device map[string]any) error {
if device != nil {
err := m.Run("chardev-add", device, nil)
if err != nil {
return err
}
}
return nil
}
// RemoveCharDevice removes a character device.
func (m *Monitor) RemoveCharDevice(deviceID string) error {
if deviceID != "" {
deviceID := map[string]string{
"id": deviceID,
}
err := m.Run("chardev-remove", deviceID, nil)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return nil
}
return err
}
}
return nil
}
// AddDevice adds a new device.
func (m *Monitor) AddDevice(device map[string]any) error {
if device != nil {
err := m.Run("device_add", device, nil)
if err != nil {
return err
}
}
return nil
}
// RemoveDevice removes a device.
func (m *Monitor) RemoveDevice(deviceID string) error {
if deviceID != "" {
deviceID := map[string]string{
"id": deviceID,
}
err := m.Run("device_del", deviceID, nil)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return nil
}
return err
}
}
return nil
}
// AddNIC adds a NIC device.
func (m *Monitor) AddNIC(netDev map[string]any, device map[string]any, connected bool) error {
reverter := revert.New()
defer reverter.Fail()
if netDev != nil {
err := m.Run("netdev_add", netDev, nil)
if err != nil {
return fmt.Errorf("Failed adding NIC netdev: %w", err)
}
reverter.Add(func() {
netDevDel := map[string]any{
"id": netDev["id"],
}
err = m.Run("netdev_del", netDevDel, nil)
if err != nil {
return
}
})
}
err := m.AddDevice(device)
if err != nil {
return fmt.Errorf("Failed adding NIC device: %w", err)
}
id, ok := device["id"].(string)
if !ok {
return errors.New("NIC device must have an id")
}
// Set link down if asked to.
if !connected {
err = m.SetNICLink(id, connected)
if err != nil {
return fmt.Errorf("Failed setting NIC device link status: %w", err)
}
}
reverter.Success()
return nil
}
// RemoveNIC removes a NIC device.
func (m *Monitor) RemoveNIC(netDevID string) error {
if netDevID != "" {
netDevID := map[string]string{
"id": netDevID,
}
err := m.Run("netdev_del", netDevID, nil)
// Not all NICs need a netdev, so if its missing, its not a problem.
if err != nil && !strings.Contains(err.Error(), "not found") {
return fmt.Errorf("Failed removing NIC netdev: %w", err)
}
}
return nil
}
// SetAction sets the actions the VM will take for certain scenarios.
func (m *Monitor) SetAction(actions map[string]string) error {
err := m.Run("set-action", actions, nil)
if err != nil {
return fmt.Errorf("Failed setting actions: %w", err)
}
return nil
}
// Reset VM.
func (m *Monitor) Reset() error {
// Announce that we're triggering a reset so the event handler can distinguish
// our deliberate system_reset from a guest-initiated reboot. This must be set
// before sending the command, since the RESET event is processed asynchronously
// and may otherwise arrive after the startup goroutine has already flipped
// the initialized flag.
m.ExpectReset()
err := m.Run("system_reset", nil, nil)
if err != nil {
m.HandleReset()
return fmt.Errorf("Failed resetting: %w", err)
}
return nil
}
// PCIClassInfo info about a device's class.
type PCIClassInfo struct {
Class int `json:"class"`
Description string `json:"desc"`
}
// PCIDevice represents a PCI device.
type PCIDevice struct {
DevID string `json:"qdev_id"`
Bus int `json:"bus"`
Slot int `json:"slot"`
Function int `json:"function"`
Devices []PCIDevice `json:"devices"`
Class PCIClassInfo `json:"class_info"`
Bridge PCIBridge `json:"pci_bridge"`
}
// PCIBridge represents a PCI bridge.
type PCIBridge struct {
Devices []PCIDevice `json:"devices"`
}
// QueryPCI returns info about PCI devices.
func (m *Monitor) QueryPCI() ([]PCIDevice, error) {
// Prepare the response.
var resp struct {
Return []struct {
Devices []PCIDevice `json:"devices"`
} `json:"return"`
}
err := m.Run("query-pci", nil, &resp)
if err != nil {
return nil, fmt.Errorf("Failed querying PCI devices: %w", err)
}
if len(resp.Return) > 0 {
return resp.Return[0].Devices, nil
}
return nil, nil
}
// BlockStats represents block device stats.
type BlockStats struct {
BytesWritten int `json:"wr_bytes"`
WritesCompleted int `json:"wr_operations"`
BytesRead int `json:"rd_bytes"`
ReadsCompleted int `json:"rd_operations"`
}
// GetBlockStats return block device stats.
func (m *Monitor) GetBlockStats() (map[string]BlockStats, error) {
// Prepare the response
var resp struct {
Return []struct {
Stats BlockStats `json:"stats"`
QDev string `json:"qdev"`
} `json:"return"`
}
err := m.Run("query-blockstats", nil, &resp)
if err != nil {
return nil, fmt.Errorf("Failed querying block stats: %w", err)
}
out := make(map[string]BlockStats)
for _, res := range resp.Return {
out[res.QDev] = res.Stats
}
return out, nil
}
// AddSecret adds a secret object with the given ID and secret. This function won't return an error
// if the secret object already exists.
func (m *Monitor) AddSecret(id string, secret string) error {
args := map[string]any{
"qom-type": "secret",
"id": id,
"data": secret,
"format": "base64",
}
err := m.Run("object-add", &args, nil)
if err != nil && !strings.Contains(err.Error(), "attempt to add duplicate property") {
return fmt.Errorf("Failed adding object: %w", err)
}
return nil
}
// AMDSEVCapabilities represents the SEV capabilities of QEMU.
type AMDSEVCapabilities struct {
PDH string `json:"pdh"` // Platform Diffie-Hellman key (base64-encoded)
CertChain string `json:"cert-chain"` // PDH certificate chain (base64-encoded)
CPU0Id string `json:"cpu0-id"` // Unique ID of CPU0 (base64-encoded)
CBitPos int `json:"cbitpos"` // C-bit location in page table entry
ReducedPhysBits int `json:"reduced-phys-bits"` // Number of physical address bit reduction when SEV is enabled
}
// SEVCapabilities is used to get the SEV capabilities, and is supported on AMD X86 platforms only.
func (m *Monitor) SEVCapabilities() (AMDSEVCapabilities, error) {
// Prepare the response
var resp struct {
Return AMDSEVCapabilities `json:"return"`
}
err := m.Run("query-sev-capabilities", nil, &resp)
if err != nil {
return AMDSEVCapabilities{}, fmt.Errorf("Failed querying SEV capability for QEMU: %w", err)
}
return resp.Return, nil
}
// NBDServerStart starts internal NBD server and returns a connection to it.
func (m *Monitor) NBDServerStart(listenPath string, maxConnections int) (net.Conn, error) {
var args struct {
Addr struct {
Data map[string]any `json:"data"`
Type string `json:"type"`
} `json:"addr"`
MaxConnections int `json:"max-connections"`
}
listenAddress := listenPath
if listenAddress == "" {
// Create abstract unix listener.
listener, err := net.Listen("unix", "")
if err != nil {
return nil, fmt.Errorf("Failed creating unix listener: %w", err)
}
// Get the random address, and then close the listener, and pass the address for use with nbd-server-start.
listenAddress = listener.Addr().String()
_ = listener.Close()
args.Addr.Type = "unix"
args.Addr.Data = map[string]any{
"path": strings.TrimPrefix(listenAddress, "@"),
"abstract": true,
}
} else {
// QEMU may have dropped privileges, bind the socket ourselves and pass the FD.
err := os.Remove(listenPath)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("Failed to remove stale socket %q: %w", listenPath, err)
}
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: listenPath, Net: "unix"})
if err != nil {
return nil, fmt.Errorf("Failed creating unix listener: %w", err)
}
listener.SetUnlinkOnClose(false)
listenerFile, err := listener.File()
_ = listener.Close()
if err != nil {
return nil, fmt.Errorf("Failed getting listener file descriptor: %w", err)
}
defer func() { _ = listenerFile.Close() }()
err = m.SendFile(listenPath, listenerFile)
if err != nil {
return nil, fmt.Errorf("Failed sending listener file descriptor: %w", err)
}
args.Addr.Type = "fd"
args.Addr.Data = map[string]any{
"str": listenPath,
}
}
args.MaxConnections = maxConnections
err := m.Run("nbd-server-start", args, nil)
if err != nil {
return nil, err
}
// Connect to the NBD server and return the connection.
conn, err := net.Dial("unix", listenAddress)
if err != nil {
return nil, fmt.Errorf("Failed connecting to NBD server: %w", err)
}
return conn, nil
}
// NBDUnixServerStart starts an internal NBD server listening on the specified Unix socket.
func (m *Monitor) NBDUnixServerStart(path string) error {
var args struct {
Addr struct {
Data struct {
Str string `json:"str"`
} `json:"data"`
Type string `json:"type"`
} `json:"addr"`
MaxConnections int `json:"max-connections"`
}
args.Addr.Type = "fd"
args.Addr.Data.Str = path
args.MaxConnections = 1
err := m.Run("nbd-server-start", args, nil)
if err != nil {
return err
}
return nil
}
// NBDServerStop stops the internal NBD server.
func (m *Monitor) NBDServerStop() error {
err := m.Run("nbd-server-stop", nil, nil)
if err != nil {
return err
}
return nil
}
// NBDBlockExportAdd adds a block export to the NBD server. Bitmaps are looked up on
// bitmapNode, which defaults to the exported node when empty.
func (m *Monitor) NBDBlockExportAdd(deviceNodeName string, exportName string, writable bool, bitmapNode string, bitmapNames []string) error {
var args struct {
ID string `json:"id"`
Type string `json:"type"`
NodeName string `json:"node-name"`
Name string `json:"name"`
Writable bool `json:"writable"`
Bitmaps []struct {
Node string `json:"node"`
Name string `json:"name"`
} `json:"bitmaps,omitempty"`
}
args.ID = deviceNodeName
args.Type = "nbd"
args.NodeName = deviceNodeName
args.Name = exportName
args.Writable = writable
if bitmapNode == "" {
bitmapNode = deviceNodeName
}
for _, b := range bitmapNames {
args.Bitmaps = append(args.Bitmaps, struct {
Node string `json:"node"`
Name string `json:"name"`
}{
Node: bitmapNode,
Name: b,
})
}
err := m.Run("block-export-add", args, nil)
if err != nil {
return err
}
return nil
}
// QueryBlock returns a list of all virtual block devices.
func (m *Monitor) QueryBlock() ([]BlockInfo, error) {
var resp struct {
Return []BlockInfo `json:"return"`
}
err := m.Run("query-block", nil, &resp)
if err != nil {
return nil, err
}
return resp.Return, nil
}
// QueryBlockExports returns exported blocks.
func (m *Monitor) QueryBlockExports() ([]BlockExport, error) {
var resp struct {
Return []BlockExport `json:"return"`
}
err := m.Run("query-block-exports", nil, &resp)
if err != nil {
return nil, err
}
return resp.Return, nil
}
// QueryNBDBlockExports returns exported blocks of type 'nbd'.
func (m *Monitor) QueryNBDBlockExports() ([]BlockExport, error) {
blocks, err := m.QueryBlockExports()
if err != nil {
return nil, err
}
result := []BlockExport{}
for _, b := range blocks {
if b.Type != "nbd" {
continue
}
result = append(result, b)
}
return result, nil
}
// QueryNamedBlockNodes returns block nodes names.
func (m *Monitor) QueryNamedBlockNodes() ([]string, error) {
var resp struct {
Return []struct {
NodeName string `json:"node-name"`
} `json:"return"`
}
err := m.Run("query-named-block-nodes", nil, &resp)
if err != nil {
return nil, err
}
result := []string{}
for _, r := range resp.Return {
result = append(result, r.NodeName)
}
return result, nil
}
// ChangeBackingFile changes backing file name for node.
func (m *Monitor) ChangeBackingFile(deviceNodeName string, imageNodeName string, backingFilename string) error {
var args struct {
Device string `json:"device"`
NodeName string `json:"image-node-name"`
BackingFile string `json:"backing-file"`
}
args.Device = deviceNodeName
args.NodeName = imageNodeName
args.BackingFile = backingFilename
err := m.Run("change-backing-file", args, nil)
if err != nil {
return err
}
return nil
}
// BlockDevSnapshot creates a snapshot of a device using the specified snapshot device.
func (m *Monitor) BlockDevSnapshot(deviceNodeName string, snapshotNodeName string) error {
var args struct {
Node string `json:"node"`
Overlay string `json:"overlay"`
}
args.Node = deviceNodeName
args.Overlay = snapshotNodeName
err := m.Run("blockdev-snapshot", args, nil)
if err != nil {
return err
}
return nil
}
// BlockDevBackupTarget describes a single blockdev-backup action.
type BlockDevBackupTarget struct {
Device string `json:"device"`
Target string `json:"target"`
Sync string `json:"sync"`
JobID string `json:"job-id"`
}
// BlockDevBackupTransaction atomically starts the given blockdev-backup jobs in a single
// transaction so that all targets share the same point in time.
func (m *Monitor) BlockDevBackupTransaction(backups []BlockDevBackupTarget) error {
type action struct {
Type string `json:"type"`
Data BlockDevBackupTarget `json:"data"`
}
actions := make([]action, 0, len(backups))
for _, backup := range backups {
actions = append(actions, action{
Type: "blockdev-backup",
Data: backup,
})
}
var args struct {
Actions []action `json:"actions"`
}
args.Actions = actions
err := m.Run("transaction", args, nil)
if err != nil {
return err
}
return nil
}
// BlockNodeSize returns the virtual size in bytes of the given block node.
func (m *Monitor) BlockNodeSize(nodeName string) (int64, error) {
var resp struct {
Return []struct {
NodeName string `json:"node-name"`
Image struct {
VirtualSize int64 `json:"virtual-size"`
} `json:"image"`
} `json:"return"`
}
err := m.Run("query-named-block-nodes", nil, &resp)
if err != nil {
return 0, err
}
for _, node := range resp.Return {
if node.NodeName == nodeName {
return node.Image.VirtualSize, nil
}
}
return 0, fmt.Errorf("Block node %q not found", nodeName)
}
// blockJobDismiss dismisses a concluded block job.
func (m *Monitor) blockJobDismiss(jobID string) error {
var args struct {
ID string `json:"id"`
}
args.ID = jobID
err := m.Run("job-dismiss", args, nil)
if err != nil {
return err
}
return nil
}
// blockJobWait waits until the specified jobID is ready (when waitReady is true), reached
// its final state or is missing. Concluded jobs are dismissed and their error, if any, is
// returned. The returned boolean is true when the job concluded rather than turned ready.
func (m *Monitor) blockJobWait(jobID string, waitReady bool, exitOnNotFound bool) (bool, error) {
for {
var resp struct {
Return []struct {
Device string `json:"device"`
Ready bool `json:"ready"`
Error string `json:"error"`
Status string `json:"status"`
} `json:"return"`
}
err := m.Run("query-block-jobs", nil, &resp)
if err != nil {
return false, err
}
found := false
for _, job := range resp.Return {
if job.Device != jobID {
continue
}
if job.Status == "concluded" {
err := m.blockJobDismiss(jobID)
if err != nil {
logger.Warn("Failed dismissing block job", logger.Ctx{"jobID": jobID, "err": err})
}
if job.Error != "" {
return true, fmt.Errorf("Failed block job: %s", job.Error)
}
return true, nil
}
if job.Error != "" {
return true, fmt.Errorf("Failed block job: %s", job.Error)
}
if waitReady && job.Ready {
return false, nil
}
found = true
}
if !found && !exitOnNotFound {
return true, errors.New("Specified block job not found")
} else if !found && exitOnNotFound {
return true, nil
}
time.Sleep(1 * time.Second)
}
}
// BlockCommit merges a snapshot device back into its parent device.
func (m *Monitor) BlockCommit(deviceNodeName string, top string, base string) error {
var args struct {
Device string `json:"device"`
JobID string `json:"job-id"`
Top string `json:"top-node,omitempty"`
Base string `json:"base-node,omitempty"`
AutoDismiss bool `json:"auto-dismiss"`
}
args.Device = deviceNodeName
args.JobID = args.Device
args.Top = top
args.Base = base
// Keep failed jobs around so their actual error can be retrieved,
// blockJobWait takes care of dismissing them.
args.AutoDismiss = false
err := m.Run("block-commit", args, nil)
if err != nil {
return err
}
// From QEMU doc: If top has no overlays on top of it, or if it is in use by a writer,
// the job will not be completed by itself.
hasTop := top != deviceNodeName && top != ""
concluded, err := m.blockJobWait(args.JobID, !hasTop, hasTop)
if err != nil {
return err
}
if !concluded {
err = m.BlockJobComplete(args.JobID)
if err != nil {
return err
}
}
return nil
}
// BlockDevMirror mirrors the top device to the target device.
func (m *Monitor) BlockDevMirror(deviceNodeName string, targetNodeName string) error {
var args struct {
Device string `json:"device"`
Target string `json:"target"`
Sync string `json:"sync"`
JobID string `json:"job-id"`
CopyMode string `json:"copy-mode"`
}
args.Device = deviceNodeName
args.Target = targetNodeName
args.JobID = deviceNodeName
// Only synchronise the top level device (usually a snapshot).
args.Sync = "top"
// When data is written to the source, write it (synchronously) to the target as well.
// In addition, data is copied in background just like in background mode.
// This ensures that the source and target converge at the cost of I/O performance during sync.
args.CopyMode = "write-blocking"
err := m.Run("blockdev-mirror", args, nil)
if err != nil {
return err
}
_, err = m.blockJobWait(args.JobID, true, false)
if err != nil {
return err
}
return nil
}
// BlockJobCancel cancels an ongoing block job.
func (m *Monitor) BlockJobCancel(deviceNodeName string) error {
var args struct {
Device string `json:"device"`
}
args.Device = deviceNodeName
err := m.Run("block-job-cancel", args, nil)
if err != nil {
return err
}
return nil
}
// BlockJobCancelWait cancels an ongoing block job and waits until it is gone.
func (m *Monitor) BlockJobCancelWait(jobID string) error {
err := m.BlockJobCancel(jobID)
if err != nil {
return err
}
_, err = m.blockJobWait(jobID, false, true)
if err != nil {
return err
}
return nil
}
// BlockJobComplete completes a block job that is in ready state.
func (m *Monitor) BlockJobComplete(deviceNodeName string) error {
var args struct {
Device string `json:"device"`
}
args.Device = deviceNodeName
err := m.Run("block-job-complete", args, nil)
if err != nil {
return err
}
// Wait for the job to reach its final state and report its result.
_, err = m.blockJobWait(args.Device, false, true)
if err != nil {
return err
}
return nil
}
// UpdateBlockSize updates the size of a disk.
func (m *Monitor) UpdateBlockSize(id string, size int64) error {
var args struct {
NodeName string `json:"node-name"`
Size int64 `json:"size"`
}
args.NodeName = id
args.Size = size
err := m.Run("block_resize", args, nil)
if err != nil {
return err
}
return nil
}
// SetBlockThrottle applies an I/O limit on a disk.
func (m *Monitor) SetBlockThrottle(id string, bytesRead int, bytesWrite int, iopsRead int, iopsWrite int) error {
var args struct {
ID string `json:"id"`
Bytes int `json:"bps"`
BytesRead int `json:"bps_rd"`
BytesWrite int `json:"bps_wr"`
IOPs int `json:"iops"`
IOPsRead int `json:"iops_rd"`
IOPsWrite int `json:"iops_wr"`
}
args.ID = id
args.BytesRead = bytesRead
args.BytesWrite = bytesWrite
args.IOPsRead = iopsRead
args.IOPsWrite = iopsWrite
err := m.Run("block_set_io_throttle", args, nil)
if err != nil {
return err
}
return nil
}
// CheckPCIDevice checks if the deviceID exists as a bridged PCI device.
func (m *Monitor) CheckPCIDevice(deviceID string) (bool, error) {
pciDevs, err := m.QueryPCI()
if err != nil {
return false, err
}
for _, pciDev := range pciDevs {
for _, bridgeDev := range pciDev.Bridge.Devices {
if bridgeDev.DevID == deviceID {
return true, nil
}
}
}
return false, nil
}
// RingbufRead returns the complete contents of the specified ring buffer.
func (m *Monitor) RingbufRead(device string) (string, error) {
// Begin by ensuring the device specified is actually a ring buffer.
var queryResp struct {
Return []struct {
Label string `json:"label"`
Filename string `json:"filename"`
FrontendOpen bool `json:"frontend_open"`
} `json:"return"`
}
err := m.Run("query-chardev", nil, &queryResp)
if err != nil {
return "", err
}
deviceFound := false
for _, qemuDevice := range queryResp.Return {
if qemuDevice.Label == device {
deviceFound = true
if qemuDevice.Filename != "ringbuf" {
// Can't call `ringbuf-read` on a non-ringbuf device.
return "", ErrNotARingbuf
}
break
}
}
if !deviceFound {
return "", fmt.Errorf("Specified qemu device %q doesn't exist", device)
}
// Now actually read from the ring buffer.
var args struct {
Device string `json:"device"`
Size int `json:"size"`
}
args.Device = device
args.Size = 10000
var readResp struct {
Return string `json:"return"`
}
var sb strings.Builder
for {
err := m.Run("ringbuf-read", args, &readResp)
if err != nil {
return "", err
}
if len(readResp.Return) == 0 {
break
}
sb.WriteString(readResp.Return)
}
return sb.String(), nil
}
// ChardevChange changes the backend of a specified chardev. Currently supports the socket and ringbuf backends.
func (m *Monitor) ChardevChange(device string, info ChardevChangeInfo) error {
switch info.Type {
case "socket":
// Share the existing file descriptor with qemu.
err := m.SendFile(info.FDName, info.File)
if err != nil {
return err
}
var args struct {
ID string `json:"id"`
Backend struct {
Type string `json:"type"`
Data struct {
Addr struct {
Type string `json:"type"`
Data struct {
Str string `json:"str"`
} `json:"data"`
} `json:"addr"`
Server bool `json:"server"`
Wait bool `json:"wait"`
} `json:"data"`
} `json:"backend"`
}
args.ID = device
args.Backend.Type = info.Type
args.Backend.Data.Addr.Type = "fd"
args.Backend.Data.Addr.Data.Str = info.FDName
args.Backend.Data.Server = true
args.Backend.Data.Wait = false
err = m.Run("chardev-change", args, nil)
if err != nil {
// If the chardev-change command failed for some reason, ensure qemu cleans up its file descriptor.
_ = m.CloseFile(info.FDName)
return err
}
return nil
case "ringbuf":
var args struct {
ID string `json:"id"`
Backend struct {
Type string `json:"type"`
Data struct {
Size int `json:"size"`
} `json:"data"`
} `json:"backend"`
}
args.ID = device
args.Backend.Type = info.Type
args.Backend.Data.Size = 1048576
return m.Run("chardev-change", args, nil)
}
return fmt.Errorf("Unsupported chardev type %q", info.Type)
}
// Screendump takes a screenshot of the current VGA console.
// The screendump is stored to the filename provided as argument.
func (m *Monitor) Screendump(filename string) error {
var args struct {
Filename string `json:"filename"`
Device string `json:"device,omitempty"`
Head int `json:"head,omitempty"`
Format string `json:"format,omitempty"`
}
args.Filename = filename
args.Format = "png"
var queryResp struct {
Return struct{} `json:"return"`
}
return m.Run("screendump", args, &queryResp)
}
// DumpGuestMemory dumps guest memory to a file.
func (m *Monitor) DumpGuestMemory(path string, format string) error {
var args struct {
Paging bool `json:"paging"`
Protocol string `json:"protocol"`
Format string `json:"format,omitempty"`
Detach bool `json:"detach"`
}
args.Protocol = "fd:" + path
args.Format = format
args.Detach = true
var queryResp struct {
Return struct{} `json:"return"`
}
err := m.Run("dump-guest-memory", args, &queryResp)
if err != nil {
return err
}
// Wait for completion.
for {
var resp struct {
Return struct {
Status string `json:"status"`
} `json:"return"`
}
err := m.Run("query-dump", nil, &resp)
if err != nil {
return err
}
switch resp.Return.Status {
case "completed":
return nil
case "failed":
return errors.New("Guest memory dump failed")
}
time.Sleep(1 * time.Second)
}
}
// SetNICLink sets the link status of the given device.
func (m *Monitor) SetNICLink(id string, connected bool) error {
var args struct {
Name string `json:"name"`
Up bool `json:"up"`
}
args.Name = id
args.Up = connected
return m.Run("set_link", args, nil)
}
// QuerySpice checks whether SPICE support is available in QEMU.
func (m *Monitor) QuerySpice() error {
return m.Run("query-spice", nil, nil)
}
// Query9pDevice checks whether virtio-9p-pci support is available in QEMU.
func (m *Monitor) Query9pDevice() error {
return m.Run("device-list-properties", map[string]string{"typename": "virtio-9p-pci"}, nil)
}
// QueryVirtioSoundDevice checks whether virtio-sound-pci support is available in QEMU.
func (m *Monitor) QueryVirtioSoundDevice() error {
return m.Run("device-list-properties", map[string]string{"typename": "virtio-sound-pci"}, nil)
}
// QueryVirtioVGADevice checks whether virtio-vga support is available in QEMU.
func (m *Monitor) QueryVirtioVGADevice() error {
return m.Run("device-list-properties", map[string]string{"typename": "virtio-vga"}, nil)
}
// AddDirtyBitmap creates a dirty bitmap for a block device.
func (m *Monitor) AddDirtyBitmap(deviceNames []string, bitmapName string, granularity int, persistent bool, disabled bool) error {
actions := []TransactionAction{}
for _, d := range deviceNames {
data := map[string]any{
"node": d,
"name": bitmapName,
"persistent": persistent,
"disabled": disabled,
}
if granularity > 0 {
data["granularity"] = granularity
}
actions = append(actions, TransactionAction{Type: "block-dirty-bitmap-add", Data: data})
}
err := m.RunTransaction(actions)
if err != nil {
return err
}
return nil
}
// RemoveDirtyBitmap removes a dirty bitmap for a block device.
func (m *Monitor) RemoveDirtyBitmap(deviceName string, bitmapName string) error {
var args struct {
Node string `json:"node"`
Name string `json:"name"`
}
args.Node = deviceName
args.Name = bitmapName
err := m.Run("block-dirty-bitmap-remove", args, nil)
if err != nil {
return err
}
return nil
}