mirror of
https://github.com/kata-containers/kata-containers.git
synced 2025-04-27 11:31:05 +00:00
Both projects follow the same license, Apache-2.0, but the header saying that comes from govmm is different from the one expected for the tests present on the kata-containers repo. Signed-off-by: Fabiano Fidêncio <fabiano.fidencio@intel.com>
1664 lines
51 KiB
Go
1664 lines
51 KiB
Go
// Copyright contributors to the Virtual Machine Manager for Go project
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
package qemu
|
|
|
|
import (
|
|
"bufio"
|
|
"container/list"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"context"
|
|
"strings"
|
|
)
|
|
|
|
// QMPLog is a logging interface used by the qemu package to log various
|
|
// interesting pieces of information. Rather than introduce a dependency
|
|
// on a given logging package, qemu presents this interface that allows
|
|
// clients to provide their own logging type which they can use to
|
|
// seamlessly integrate qemu's logs into their own logs. A QMPLog
|
|
// implementation can be specified in the QMPConfig structure.
|
|
type QMPLog interface {
|
|
// V returns true if the given argument is less than or equal
|
|
// to the implementation's defined verbosity level.
|
|
V(int32) bool
|
|
|
|
// Infof writes informational output to the log. A newline will be
|
|
// added to the output if one is not provided.
|
|
Infof(string, ...interface{})
|
|
|
|
// Warningf writes warning output to the log. A newline will be
|
|
// added to the output if one is not provided.
|
|
Warningf(string, ...interface{})
|
|
|
|
// Errorf writes error output to the log. A newline will be
|
|
// added to the output if one is not provided.
|
|
Errorf(string, ...interface{})
|
|
}
|
|
|
|
type qmpNullLogger struct{}
|
|
|
|
func (l qmpNullLogger) V(level int32) bool {
|
|
return false
|
|
}
|
|
|
|
func (l qmpNullLogger) Infof(format string, v ...interface{}) {
|
|
}
|
|
|
|
func (l qmpNullLogger) Warningf(format string, v ...interface{}) {
|
|
}
|
|
|
|
func (l qmpNullLogger) Errorf(format string, v ...interface{}) {
|
|
}
|
|
|
|
// QMPConfig is a configuration structure that can be used to specify a
|
|
// logger and a channel to which logs and QMP events are to be sent. If
|
|
// neither of these fields are specified, or are set to nil, no logs will be
|
|
// written and no QMP events will be reported to the client.
|
|
type QMPConfig struct {
|
|
// eventCh can be specified by clients who wish to receive QMP
|
|
// events.
|
|
EventCh chan<- QMPEvent
|
|
|
|
// logger is used by the qmpStart function and all the go routines
|
|
// it spawns to log information.
|
|
Logger QMPLog
|
|
|
|
// specify the capacity of buffer used by receive QMP response.
|
|
MaxCapacity int
|
|
}
|
|
|
|
type qmpEventFilter struct {
|
|
eventName string
|
|
dataKey string
|
|
dataValue string
|
|
}
|
|
|
|
// QMPEvent contains a single QMP event, sent on the QMPConfig.EventCh channel.
|
|
// nolint: govet
|
|
type QMPEvent struct {
|
|
// The name of the event, e.g., DEVICE_DELETED
|
|
Name string
|
|
|
|
// The data associated with the event. The contents of this map are
|
|
// unprocessed by the qemu package. It is simply the result of
|
|
// unmarshalling the QMP json event. Here's an example map
|
|
// map[string]interface{}{
|
|
// "driver": "virtio-blk-pci",
|
|
// "drive": "drive_3437843748734873483",
|
|
// }
|
|
Data map[string]interface{}
|
|
|
|
// The event's timestamp converted to a time.Time object.
|
|
Timestamp time.Time
|
|
}
|
|
|
|
type qmpResult struct {
|
|
response interface{}
|
|
err error
|
|
}
|
|
|
|
// nolint: govet
|
|
type qmpCommand struct {
|
|
ctx context.Context
|
|
res chan qmpResult
|
|
name string
|
|
args map[string]interface{}
|
|
filter *qmpEventFilter
|
|
resultReceived bool
|
|
oob []byte
|
|
}
|
|
|
|
// QMP is a structure that contains the internal state used by startQMPLoop and
|
|
// the go routines it spwans. All the contents of this structure are private.
|
|
// nolint: govet
|
|
type QMP struct {
|
|
cmdCh chan qmpCommand
|
|
conn io.ReadWriteCloser
|
|
cfg QMPConfig
|
|
connectedCh chan<- *QMPVersion
|
|
disconnectedCh chan struct{}
|
|
version *QMPVersion
|
|
}
|
|
|
|
// QMPVersion contains the version number and the capabailities of a QEMU
|
|
// instance, as reported in the QMP greeting message.
|
|
// nolint: govet
|
|
type QMPVersion struct {
|
|
Major int
|
|
Minor int
|
|
Micro int
|
|
Capabilities []string
|
|
}
|
|
|
|
// CPUProperties contains the properties of a CPU instance
|
|
type CPUProperties struct {
|
|
Node int `json:"node-id"`
|
|
Socket int `json:"socket-id"`
|
|
Die int `json:"die-id"`
|
|
Core int `json:"core-id"`
|
|
Thread int `json:"thread-id"`
|
|
}
|
|
|
|
// HotpluggableCPU represents a hotpluggable CPU
|
|
// nolint: govet
|
|
type HotpluggableCPU struct {
|
|
Type string `json:"type"`
|
|
VcpusCount int `json:"vcpus-count"`
|
|
Properties CPUProperties `json:"props"`
|
|
QOMPath string `json:"qom-path"`
|
|
}
|
|
|
|
// MemoryDevicesData cotains the data describes a memory device
|
|
// nolint: govet
|
|
type MemoryDevicesData struct {
|
|
Slot int `json:"slot"`
|
|
Node int `json:"node"`
|
|
Addr uint64 `json:"addr"`
|
|
Memdev string `json:"memdev"`
|
|
ID string `json:"id"`
|
|
Hotpluggable bool `json:"hotpluggable"`
|
|
Hotplugged bool `json:"hotplugged"`
|
|
Size uint64 `json:"size"`
|
|
}
|
|
|
|
// MemoryDevices represents memory devices of vm
|
|
// nolint: govet
|
|
type MemoryDevices struct {
|
|
Data MemoryDevicesData `json:"data"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// CPUInfo represents information about each virtual CPU
|
|
// nolint: govet
|
|
type CPUInfo struct {
|
|
CPU int `json:"CPU"`
|
|
Current bool `json:"current"`
|
|
Halted bool `json:"halted"`
|
|
QomPath string `json:"qom_path"`
|
|
Arch string `json:"arch"`
|
|
Pc int `json:"pc"`
|
|
ThreadID int `json:"thread_id"`
|
|
Props CPUProperties `json:"props"`
|
|
}
|
|
|
|
// CPUInfoFast represents information about each virtual CPU
|
|
// nolint: govet
|
|
type CPUInfoFast struct {
|
|
CPUIndex int `json:"cpu-index"`
|
|
QomPath string `json:"qom-path"`
|
|
Arch string `json:"arch"`
|
|
ThreadID int `json:"thread-id"`
|
|
Target string `json:"target"`
|
|
Props CPUProperties `json:"props"`
|
|
}
|
|
|
|
// MigrationRAM represents migration ram status
|
|
type MigrationRAM struct {
|
|
Total int64 `json:"total"`
|
|
Remaining int64 `json:"remaining"`
|
|
Transferred int64 `json:"transferred"`
|
|
TotalTime int64 `json:"total-time"`
|
|
SetupTime int64 `json:"setup-time"`
|
|
ExpectedDowntime int64 `json:"expected-downtime"`
|
|
Duplicate int64 `json:"duplicate"`
|
|
Normal int64 `json:"normal"`
|
|
NormalBytes int64 `json:"normal-bytes"`
|
|
DirtySyncCount int64 `json:"dirty-sync-count"`
|
|
}
|
|
|
|
// MigrationDisk represents migration disk status
|
|
type MigrationDisk struct {
|
|
Total int64 `json:"total"`
|
|
Remaining int64 `json:"remaining"`
|
|
Transferred int64 `json:"transferred"`
|
|
}
|
|
|
|
// MigrationXbzrleCache represents migration XbzrleCache status
|
|
type MigrationXbzrleCache struct {
|
|
CacheSize int64 `json:"cache-size"`
|
|
Bytes int64 `json:"bytes"`
|
|
Pages int64 `json:"pages"`
|
|
CacheMiss int64 `json:"cache-miss"`
|
|
CacheMissRate int64 `json:"cache-miss-rate"`
|
|
Overflow int64 `json:"overflow"`
|
|
}
|
|
|
|
// MigrationStatus represents migration status of a vm
|
|
type MigrationStatus struct {
|
|
Status string `json:"status"`
|
|
Capabilities []map[string]interface{} `json:"capabilities,omitempty"`
|
|
RAM MigrationRAM `json:"ram,omitempty"`
|
|
Disk MigrationDisk `json:"disk,omitempty"`
|
|
XbzrleCache MigrationXbzrleCache `json:"xbzrle-cache,omitempty"`
|
|
}
|
|
|
|
// SchemaInfo represents all QMP wire ABI
|
|
type SchemaInfo struct {
|
|
MetaType string `json:"meta-type"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// StatusInfo represents guest running status
|
|
// nolint: govet
|
|
type StatusInfo struct {
|
|
Running bool `json:"running"`
|
|
SingleStep bool `json:"singlestep"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (q *QMP) readLoop(fromVMCh chan<- []byte) {
|
|
scanner := bufio.NewScanner(q.conn)
|
|
if q.cfg.MaxCapacity > 0 {
|
|
buffer := make([]byte, q.cfg.MaxCapacity)
|
|
scanner.Buffer(buffer, q.cfg.MaxCapacity)
|
|
}
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Bytes()
|
|
// Since []byte channel type transfer slice info(include slice underlying array pointer, len, cap)
|
|
// between channel sender and receiver. scanner.Bytes() returned slice's underlying array
|
|
// may point to data that will be overwritten by a subsequent call to Scan(reference from:
|
|
// https://golang.org/pkg/bufio/#Scanner.Bytes), which may make receiver read mixed data,
|
|
// so we need to copy line to new allocated space and then send to channel receiver
|
|
sendLine := make([]byte, len(line))
|
|
copy(sendLine, line)
|
|
|
|
fromVMCh <- sendLine
|
|
}
|
|
q.cfg.Logger.Infof("scanner return error: %v", scanner.Err())
|
|
close(fromVMCh)
|
|
}
|
|
|
|
func (q *QMP) processQMPEvent(cmdQueue *list.List, name interface{}, data interface{},
|
|
timestamp interface{}) {
|
|
|
|
strname, ok := name.(string)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var eventData map[string]interface{}
|
|
if data != nil {
|
|
eventData, _ = data.(map[string]interface{})
|
|
}
|
|
|
|
cmdEl := cmdQueue.Front()
|
|
if cmdEl != nil {
|
|
cmd := cmdEl.Value.(*qmpCommand)
|
|
filter := cmd.filter
|
|
if filter != nil {
|
|
if filter.eventName == strname {
|
|
match := filter.dataKey == ""
|
|
if !match && eventData != nil {
|
|
match = eventData[filter.dataKey] == filter.dataValue
|
|
}
|
|
if match {
|
|
if cmd.resultReceived {
|
|
q.finaliseCommand(cmdEl, cmdQueue, true)
|
|
} else {
|
|
cmd.filter = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if q.cfg.EventCh != nil {
|
|
ev := QMPEvent{
|
|
Name: strname,
|
|
Data: eventData,
|
|
}
|
|
if timestamp != nil {
|
|
timestamp, ok := timestamp.(map[string]interface{})
|
|
if ok {
|
|
seconds, _ := timestamp["seconds"].(float64)
|
|
microseconds, _ := timestamp["microseconds"].(float64)
|
|
ev.Timestamp = time.Unix(int64(seconds), int64(microseconds))
|
|
}
|
|
}
|
|
|
|
q.cfg.EventCh <- ev
|
|
}
|
|
}
|
|
|
|
func (q *QMP) finaliseCommandWithResponse(cmdEl *list.Element, cmdQueue *list.List, succeeded bool, response interface{}) {
|
|
cmd := cmdEl.Value.(*qmpCommand)
|
|
cmdQueue.Remove(cmdEl)
|
|
select {
|
|
case <-cmd.ctx.Done():
|
|
default:
|
|
if succeeded {
|
|
cmd.res <- qmpResult{response: response}
|
|
} else {
|
|
cmd.res <- qmpResult{err: fmt.Errorf("QMP command failed: %v", response)}
|
|
}
|
|
}
|
|
if cmdQueue.Len() > 0 {
|
|
q.writeNextQMPCommand(cmdQueue)
|
|
}
|
|
}
|
|
|
|
func (q *QMP) finaliseCommand(cmdEl *list.Element, cmdQueue *list.List, succeeded bool) {
|
|
q.finaliseCommandWithResponse(cmdEl, cmdQueue, succeeded, nil)
|
|
}
|
|
|
|
func (q *QMP) errorDesc(errorData interface{}) (string, error) {
|
|
// convert error to json
|
|
data, err := json.Marshal(errorData)
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to extract error information: %v", err)
|
|
}
|
|
|
|
// see: https://github.com/qemu/qemu/blob/stable-2.12/qapi/qmp-dispatch.c#L125
|
|
var qmpErr map[string]string
|
|
// convert json to qmpError
|
|
if err = json.Unmarshal(data, &qmpErr); err != nil {
|
|
return "", fmt.Errorf("unable to convert json to qmpError: %v", err)
|
|
}
|
|
|
|
return qmpErr["desc"], nil
|
|
}
|
|
|
|
func (q *QMP) processQMPInput(line []byte, cmdQueue *list.List) {
|
|
var vmData map[string]interface{}
|
|
err := json.Unmarshal(line, &vmData)
|
|
if err != nil {
|
|
q.cfg.Logger.Warningf("Unable to decode response [%s] from VM: %v",
|
|
string(line), err)
|
|
return
|
|
}
|
|
if evname, found := vmData["event"]; found {
|
|
q.processQMPEvent(cmdQueue, evname, vmData["data"], vmData["timestamp"])
|
|
return
|
|
}
|
|
|
|
response, succeeded := vmData["return"]
|
|
errData, failed := vmData["error"]
|
|
|
|
if !succeeded && !failed {
|
|
return
|
|
}
|
|
|
|
cmdEl := cmdQueue.Front()
|
|
if cmdEl == nil {
|
|
q.cfg.Logger.Warningf("Unexpected command response received [%s] from VM",
|
|
string(line))
|
|
return
|
|
}
|
|
cmd := cmdEl.Value.(*qmpCommand)
|
|
if failed || cmd.filter == nil {
|
|
if errData != nil {
|
|
desc, err := q.errorDesc(errData)
|
|
if err != nil {
|
|
q.cfg.Logger.Infof("Get error description failed: %v", err)
|
|
} else {
|
|
response = desc
|
|
}
|
|
}
|
|
q.finaliseCommandWithResponse(cmdEl, cmdQueue, succeeded, response)
|
|
} else {
|
|
cmd.resultReceived = true
|
|
}
|
|
}
|
|
|
|
func currentCommandDoneCh(cmdQueue *list.List) <-chan struct{} {
|
|
cmdEl := cmdQueue.Front()
|
|
if cmdEl == nil {
|
|
return nil
|
|
}
|
|
cmd := cmdEl.Value.(*qmpCommand)
|
|
return cmd.ctx.Done()
|
|
}
|
|
|
|
func (q *QMP) writeNextQMPCommand(cmdQueue *list.List) {
|
|
cmdEl := cmdQueue.Front()
|
|
cmd := cmdEl.Value.(*qmpCommand)
|
|
cmdData := make(map[string]interface{})
|
|
cmdData["execute"] = cmd.name
|
|
if cmd.args != nil {
|
|
cmdData["arguments"] = cmd.args
|
|
}
|
|
encodedCmd, err := json.Marshal(&cmdData)
|
|
if err != nil {
|
|
cmd.res <- qmpResult{
|
|
err: fmt.Errorf("unable to marhsall command %s: %v",
|
|
cmd.name, err),
|
|
}
|
|
cmdQueue.Remove(cmdEl)
|
|
}
|
|
encodedCmd = append(encodedCmd, '\n')
|
|
if unixConn, ok := q.conn.(*net.UnixConn); ok && len(cmd.oob) > 0 {
|
|
_, _, err = unixConn.WriteMsgUnix(encodedCmd, cmd.oob, nil)
|
|
} else {
|
|
_, err = q.conn.Write(encodedCmd)
|
|
}
|
|
|
|
if err != nil {
|
|
cmd.res <- qmpResult{
|
|
err: fmt.Errorf("unable to write command to qmp socket %v", err),
|
|
}
|
|
cmdQueue.Remove(cmdEl)
|
|
}
|
|
}
|
|
|
|
func failOutstandingCommands(cmdQueue *list.List) {
|
|
for e := cmdQueue.Front(); e != nil; e = e.Next() {
|
|
cmd := e.Value.(*qmpCommand)
|
|
select {
|
|
case cmd.res <- qmpResult{
|
|
err: errors.New("exitting QMP loop, command cancelled"),
|
|
}:
|
|
case <-cmd.ctx.Done():
|
|
}
|
|
}
|
|
}
|
|
|
|
func (q *QMP) cancelCurrentCommand(cmdQueue *list.List) {
|
|
cmdEl := cmdQueue.Front()
|
|
cmd := cmdEl.Value.(*qmpCommand)
|
|
if cmd.resultReceived {
|
|
q.finaliseCommand(cmdEl, cmdQueue, false)
|
|
} else {
|
|
cmd.filter = nil
|
|
}
|
|
}
|
|
|
|
func (q *QMP) parseVersion(version []byte) *QMPVersion {
|
|
var qmp map[string]interface{}
|
|
err := json.Unmarshal(version, &qmp)
|
|
if err != nil {
|
|
q.cfg.Logger.Errorf("Invalid QMP greeting: %s", string(version))
|
|
return nil
|
|
}
|
|
|
|
versionMap := qmp
|
|
for _, k := range []string{"QMP", "version", "qemu"} {
|
|
versionMap, _ = versionMap[k].(map[string]interface{})
|
|
if versionMap == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
micro, _ := versionMap["micro"].(float64)
|
|
minor, _ := versionMap["minor"].(float64)
|
|
major, _ := versionMap["major"].(float64)
|
|
capabilities, _ := qmp["QMP"].(map[string]interface{})["capabilities"].([]interface{})
|
|
stringcaps := make([]string, 0, len(capabilities))
|
|
for _, c := range capabilities {
|
|
if cap, ok := c.(string); ok {
|
|
stringcaps = append(stringcaps, cap)
|
|
}
|
|
}
|
|
return &QMPVersion{Major: int(major),
|
|
Minor: int(minor),
|
|
Micro: int(micro),
|
|
Capabilities: stringcaps,
|
|
}
|
|
}
|
|
|
|
// The qemu package allows multiple QMP commands to be submitted concurrently
|
|
// from different Go routines. Unfortunately, QMP doesn't really support parallel
|
|
// commands as there is no way reliable way to associate a command response
|
|
// with a request. For this reason we need to submit our commands to
|
|
// QMP serially. The qemu package performs this serialisation using a
|
|
// queue (cmdQueue owned by mainLoop). We use a queue rather than a simple
|
|
// mutex so we can support cancelling of commands (see below) and ordered
|
|
// execution of commands, i.e., if command B is issued before command C,
|
|
// it should be executed before command C even if both commands are initially
|
|
// blocked waiting for command A to finish. This would be hard to achieve with
|
|
// a simple mutex.
|
|
//
|
|
// Cancelling is a little tricky. Commands such as ExecuteQMPCapabilities
|
|
// can be cancelled by cancelling or timing out their contexts. When a
|
|
// command is cancelled the calling function, e.g., ExecuteQMPCapabilities,
|
|
// will return but we may not be able to remove the command's entry from
|
|
// the command queue or issue the next command. There are two scenarios
|
|
// here.
|
|
//
|
|
// 1. The command has been processed by QMP, i.e., we have received a
|
|
// return or an error, but is still blocking as it is waiting for
|
|
// an event. For example, the ExecuteDeviceDel blocks until a DEVICE_DELETED
|
|
// event is received. When such a command is cancelled we can remove it
|
|
// from the queue and start issuing the next command. When the DEVICE_DELETED
|
|
// event eventually arrives it will just be ignored.
|
|
//
|
|
// 2. The command has not been processed by QMP. In this case the command
|
|
// needs to remain on the cmdQueue until the response to this command is
|
|
// received from QMP. During this time no new commands can be issued. When the
|
|
// response is received, it is discarded (as no one is interested in the result
|
|
// any more), the entry is removed from the cmdQueue and we can proceed to
|
|
// execute the next command.
|
|
|
|
func (q *QMP) mainLoop() {
|
|
cmdQueue := list.New().Init()
|
|
fromVMCh := make(chan []byte)
|
|
go q.readLoop(fromVMCh)
|
|
|
|
defer func() {
|
|
if q.cfg.EventCh != nil {
|
|
close(q.cfg.EventCh)
|
|
}
|
|
/* #nosec */
|
|
_ = q.conn.Close()
|
|
<-fromVMCh
|
|
failOutstandingCommands(cmdQueue)
|
|
close(q.disconnectedCh)
|
|
}()
|
|
|
|
var cmdDoneCh <-chan struct{}
|
|
var version *QMPVersion
|
|
ready := false
|
|
|
|
for {
|
|
select {
|
|
case cmd, ok := <-q.cmdCh:
|
|
if !ok {
|
|
return
|
|
}
|
|
_ = cmdQueue.PushBack(&cmd)
|
|
|
|
// We only want to execute the new cmd if QMP is
|
|
// ready and there are no other commands pending.
|
|
// If there are commands pending our new command
|
|
// will get run when the pending commands complete.
|
|
if ready && cmdQueue.Len() == 1 {
|
|
q.writeNextQMPCommand(cmdQueue)
|
|
cmdDoneCh = currentCommandDoneCh(cmdQueue)
|
|
}
|
|
|
|
case line, ok := <-fromVMCh:
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if !ready {
|
|
// Not ready yet. Check if line is the QMP version.
|
|
// Sometimes QMP events are thrown before the QMP version,
|
|
// hence it's not a guarantee that the first data read from
|
|
// the channel is the QMP version.
|
|
version = q.parseVersion(line)
|
|
if version != nil {
|
|
q.connectedCh <- version
|
|
ready = true
|
|
}
|
|
// Do not process QMP input to avoid deadlocks.
|
|
break
|
|
}
|
|
|
|
q.processQMPInput(line, cmdQueue)
|
|
cmdDoneCh = currentCommandDoneCh(cmdQueue)
|
|
|
|
case <-cmdDoneCh:
|
|
q.cancelCurrentCommand(cmdQueue)
|
|
cmdDoneCh = currentCommandDoneCh(cmdQueue)
|
|
}
|
|
}
|
|
}
|
|
|
|
func startQMPLoop(conn io.ReadWriteCloser, cfg QMPConfig,
|
|
connectedCh chan<- *QMPVersion, disconnectedCh chan struct{}) *QMP {
|
|
q := &QMP{
|
|
cmdCh: make(chan qmpCommand),
|
|
conn: conn,
|
|
cfg: cfg,
|
|
connectedCh: connectedCh,
|
|
disconnectedCh: disconnectedCh,
|
|
}
|
|
go q.mainLoop()
|
|
return q
|
|
}
|
|
|
|
func (q *QMP) executeCommandWithResponse(ctx context.Context, name string, args map[string]interface{},
|
|
oob []byte, filter *qmpEventFilter) (interface{}, error) {
|
|
var err error
|
|
var response interface{}
|
|
resCh := make(chan qmpResult)
|
|
select {
|
|
case <-q.disconnectedCh:
|
|
err = errors.New("exitting QMP loop, command cancelled")
|
|
case q.cmdCh <- qmpCommand{
|
|
ctx: ctx,
|
|
res: resCh,
|
|
name: name,
|
|
args: args,
|
|
filter: filter,
|
|
oob: oob,
|
|
}:
|
|
}
|
|
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
select {
|
|
case res := <-resCh:
|
|
err = res.err
|
|
response = res.response
|
|
case <-ctx.Done():
|
|
err = ctx.Err()
|
|
}
|
|
|
|
return response, err
|
|
}
|
|
|
|
func (q *QMP) executeCommand(ctx context.Context, name string, args map[string]interface{},
|
|
filter *qmpEventFilter) error {
|
|
|
|
_, err := q.executeCommandWithResponse(ctx, name, args, nil, filter)
|
|
return err
|
|
}
|
|
|
|
// QMPStart connects to a unix domain socket maintained by a QMP instance. It
|
|
// waits to receive the QMP welcome message via the socket and spawns some go
|
|
// routines to manage the socket. The function returns a *QMP which can be
|
|
// used by callers to send commands to the QEMU instance or to close the
|
|
// socket and all the go routines that have been spawned to monitor it. A
|
|
// *QMPVersion is also returned. This structure contains the version and
|
|
// capabilities information returned by the QEMU instance in its welcome
|
|
// message.
|
|
//
|
|
// socket contains the path to the domain socket. cfg contains some options
|
|
// that can be specified by the caller, namely where the qemu package should
|
|
// send logs and QMP events. disconnectedCh is a channel that must be supplied
|
|
// by the caller. It is closed when an error occurs openning or writing to
|
|
// or reading from the unix domain socket. This implies that the QEMU instance
|
|
// that opened the socket has closed.
|
|
//
|
|
// If this function returns without error, callers should call QMP.Shutdown
|
|
// when they wish to stop monitoring the QMP instance. This is not strictly
|
|
// necessary if the QEMU instance exits and the disconnectedCh is closed, but
|
|
// doing so will not cause any problems.
|
|
//
|
|
// Commands can be sent to the QEMU instance via the QMP.Execute methods.
|
|
// These commands are executed serially, even if the QMP.Execute methods
|
|
// are called from different go routines. The QMP.Execute methods will
|
|
// block until they have received a success or failure message from QMP,
|
|
// i.e., {"return": {}} or {"error":{}}, and in some cases certain events
|
|
// are received.
|
|
//
|
|
// QEMU currently requires that the "qmp_capabilties" command is sent before any
|
|
// other command. Therefore you must call qmp.ExecuteQMPCapabilities() before
|
|
// you execute any other command.
|
|
func QMPStart(ctx context.Context, socket string, cfg QMPConfig, disconnectedCh chan struct{}) (*QMP, *QMPVersion, error) {
|
|
if cfg.Logger == nil {
|
|
cfg.Logger = qmpNullLogger{}
|
|
}
|
|
dialer := net.Dialer{Cancel: ctx.Done()}
|
|
conn, err := dialer.Dial("unix", socket)
|
|
if err != nil {
|
|
cfg.Logger.Warningf("Unable to connect to unix socket (%s): %v", socket, err)
|
|
close(disconnectedCh)
|
|
return nil, nil, err
|
|
}
|
|
|
|
connectedCh := make(chan *QMPVersion)
|
|
|
|
q := startQMPLoop(conn, cfg, connectedCh, disconnectedCh)
|
|
select {
|
|
case <-ctx.Done():
|
|
q.Shutdown()
|
|
<-disconnectedCh
|
|
return nil, nil, fmt.Errorf("canceled by caller")
|
|
case <-disconnectedCh:
|
|
return nil, nil, fmt.Errorf("lost connection to VM")
|
|
case q.version = <-connectedCh:
|
|
if q.version == nil {
|
|
return nil, nil, fmt.Errorf("failed to find QMP version information")
|
|
}
|
|
}
|
|
|
|
if q.version.Major < 5 {
|
|
return nil, nil, fmt.Errorf("govmm requires qemu version 5.0 or later, this is qemu (%d.%d)", q.version.Major, q.version.Minor)
|
|
}
|
|
|
|
return q, q.version, nil
|
|
}
|
|
|
|
// Shutdown closes the domain socket used to monitor a QEMU instance and
|
|
// terminates all the go routines spawned by QMPStart to manage that instance.
|
|
// QMP.Shutdown does not shut down the running instance. Calling QMP.Shutdown
|
|
// will result in the disconnectedCh channel being closed, indicating that we
|
|
// have lost connection to the QMP instance. In this case it does not indicate
|
|
// that the instance has quit.
|
|
//
|
|
// QMP.Shutdown should not be called concurrently with other QMP methods. It
|
|
// should not be called twice on the same QMP instance.
|
|
//
|
|
// Calling QMP.Shutdown after the disconnectedCh channel is closed is permitted but
|
|
// will not have any effect.
|
|
func (q *QMP) Shutdown() {
|
|
close(q.cmdCh)
|
|
}
|
|
|
|
// ExecuteQMPCapabilities executes the qmp_capabilities command on the instance.
|
|
func (q *QMP) ExecuteQMPCapabilities(ctx context.Context) error {
|
|
return q.executeCommand(ctx, "qmp_capabilities", nil, nil)
|
|
}
|
|
|
|
// ExecuteStop sends the stop command to the instance.
|
|
func (q *QMP) ExecuteStop(ctx context.Context) error {
|
|
return q.executeCommand(ctx, "stop", nil, nil)
|
|
}
|
|
|
|
// ExecuteCont sends the cont command to the instance.
|
|
func (q *QMP) ExecuteCont(ctx context.Context) error {
|
|
return q.executeCommand(ctx, "cont", nil, nil)
|
|
}
|
|
|
|
// ExecuteSystemPowerdown sends the system_powerdown command to the instance.
|
|
// This function will block until the SHUTDOWN event is received.
|
|
func (q *QMP) ExecuteSystemPowerdown(ctx context.Context) error {
|
|
filter := &qmpEventFilter{
|
|
eventName: "POWERDOWN",
|
|
}
|
|
return q.executeCommand(ctx, "system_powerdown", nil, filter)
|
|
}
|
|
|
|
// ExecuteQuit sends the quit command to the instance, terminating
|
|
// the QMP instance immediately.
|
|
func (q *QMP) ExecuteQuit(ctx context.Context) error {
|
|
return q.executeCommand(ctx, "quit", nil, nil)
|
|
}
|
|
|
|
func (q *QMP) blockdevAddBaseArgs(driver, device, blockdevID string, ro bool) (map[string]interface{}, map[string]interface{}) {
|
|
var args map[string]interface{}
|
|
|
|
blockdevArgs := map[string]interface{}{
|
|
"driver": "raw",
|
|
"read-only": ro,
|
|
"file": map[string]interface{}{
|
|
"driver": driver,
|
|
"filename": device,
|
|
},
|
|
}
|
|
|
|
blockdevArgs["node-name"] = blockdevID
|
|
args = blockdevArgs
|
|
|
|
return args, blockdevArgs
|
|
}
|
|
|
|
// ExecuteBlockdevAdd sends a blockdev-add to the QEMU instance. device is the
|
|
// path of the device to add, e.g., /dev/rdb0, and blockdevID is an identifier
|
|
// used to name the device. As this identifier will be passed directly to QMP,
|
|
// it must obey QMP's naming rules, e,g., it must start with a letter.
|
|
func (q *QMP) ExecuteBlockdevAdd(ctx context.Context, device, blockdevID string, ro bool) error {
|
|
args, _ := q.blockdevAddBaseArgs("host_device", device, blockdevID, ro)
|
|
|
|
return q.executeCommand(ctx, "blockdev-add", args, nil)
|
|
}
|
|
|
|
// ExecuteBlockdevAddWithCache has two more parameters direct and noFlush
|
|
// than ExecuteBlockdevAdd.
|
|
// They are cache-related options for block devices that are described in
|
|
// https://github.com/qemu/qemu/blob/master/qapi/block-core.json.
|
|
// direct denotes whether use of O_DIRECT (bypass the host page cache)
|
|
// is enabled. noFlush denotes whether flush requests for the device are
|
|
// ignored.
|
|
func (q *QMP) ExecuteBlockdevAddWithCache(ctx context.Context, device, blockdevID string, direct, noFlush, ro bool) error {
|
|
args, blockdevArgs := q.blockdevAddBaseArgs("host_device", device, blockdevID, ro)
|
|
|
|
blockdevArgs["cache"] = map[string]interface{}{
|
|
"direct": direct,
|
|
"no-flush": noFlush,
|
|
}
|
|
|
|
return q.executeCommand(ctx, "blockdev-add", args, nil)
|
|
}
|
|
|
|
// ExecuteBlockdevAddWithDriverCache has three one parameter driver
|
|
// than ExecuteBlockdevAddWithCache.
|
|
// Parameter driver can set the driver of block device.
|
|
func (q *QMP) ExecuteBlockdevAddWithDriverCache(ctx context.Context, driver, device, blockdevID string, direct, noFlush, ro bool) error {
|
|
args, blockdevArgs := q.blockdevAddBaseArgs(driver, device, blockdevID, ro)
|
|
|
|
blockdevArgs["cache"] = map[string]interface{}{
|
|
"direct": direct,
|
|
"no-flush": noFlush,
|
|
}
|
|
|
|
return q.executeCommand(ctx, "blockdev-add", args, nil)
|
|
}
|
|
|
|
// ExecuteDeviceAdd adds the guest portion of a device to a QEMU instance
|
|
// using the device_add command. blockdevID should match the blockdevID passed
|
|
// to a previous call to ExecuteBlockdevAdd. devID is the id of the device to
|
|
// add. Both strings must be valid QMP identifiers. driver is the name of the
|
|
// driver,e.g., virtio-blk-pci, and bus is the name of the bus. bus is optional.
|
|
// shared denotes if the drive can be shared allowing it to be passed more than once.
|
|
// disableModern indicates if virtio version 1.0 should be replaced by the
|
|
// former version 0.9, as there is a KVM bug that occurs when using virtio
|
|
// 1.0 in nested environments.
|
|
func (q *QMP) ExecuteDeviceAdd(ctx context.Context, blockdevID, devID, driver, bus, romfile string, shared, disableModern bool) error {
|
|
args := map[string]interface{}{
|
|
"id": devID,
|
|
"driver": driver,
|
|
"drive": blockdevID,
|
|
}
|
|
|
|
var transport VirtioTransport
|
|
|
|
if transport.isVirtioCCW(nil) {
|
|
args["devno"] = bus
|
|
} else if bus != "" {
|
|
args["bus"] = bus
|
|
}
|
|
|
|
if shared {
|
|
args["share-rw"] = "on"
|
|
}
|
|
if transport.isVirtioPCI(nil) {
|
|
args["romfile"] = romfile
|
|
|
|
if disableModern {
|
|
args["disable-modern"] = disableModern
|
|
}
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteSCSIDeviceAdd adds the guest portion of a block device to a QEMU instance
|
|
// using a SCSI driver with the device_add command. blockdevID should match the
|
|
// blockdevID passed to a previous call to ExecuteBlockdevAdd. devID is the id of
|
|
// the device to add. Both strings must be valid QMP identifiers. driver is the name of the
|
|
// scsi driver,e.g., scsi-hd, and bus is the name of a SCSI controller bus.
|
|
// scsiID is the SCSI id, lun is logical unit number. scsiID and lun are optional, a negative value
|
|
// for scsiID and lun is ignored. shared denotes if the drive can be shared allowing it
|
|
// to be passed more than once.
|
|
// disableModern indicates if virtio version 1.0 should be replaced by the
|
|
// former version 0.9, as there is a KVM bug that occurs when using virtio
|
|
// 1.0 in nested environments.
|
|
func (q *QMP) ExecuteSCSIDeviceAdd(ctx context.Context, blockdevID, devID, driver, bus, romfile string, scsiID, lun int, shared, disableModern bool) error {
|
|
// TBD: Add drivers for scsi passthrough like scsi-generic and scsi-block
|
|
drivers := []string{"scsi-hd", "scsi-cd", "scsi-disk"}
|
|
|
|
isSCSIDriver := false
|
|
for _, d := range drivers {
|
|
if driver == d {
|
|
isSCSIDriver = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isSCSIDriver {
|
|
return fmt.Errorf("invalid SCSI driver provided %s", driver)
|
|
}
|
|
|
|
args := map[string]interface{}{
|
|
"id": devID,
|
|
"driver": driver,
|
|
"drive": blockdevID,
|
|
"bus": bus,
|
|
}
|
|
|
|
if scsiID >= 0 {
|
|
args["scsi-id"] = scsiID
|
|
}
|
|
if lun >= 0 {
|
|
args["lun"] = lun
|
|
}
|
|
if shared {
|
|
args["share-rw"] = "on"
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteBlockdevDel deletes a block device by sending blockdev-del
|
|
// command. blockdevID is the id of the block device to be deleted.
|
|
// Typically, this will match the id passed to ExecuteBlockdevAdd. It
|
|
// must be a valid QMP id.
|
|
func (q *QMP) ExecuteBlockdevDel(ctx context.Context, blockdevID string) error {
|
|
args := map[string]interface{}{}
|
|
|
|
args["node-name"] = blockdevID
|
|
return q.executeCommand(ctx, "blockdev-del", args, nil)
|
|
}
|
|
|
|
// ExecuteChardevDel deletes a char device by sending a chardev-remove command.
|
|
// chardevID is the id of the char device to be deleted. Typically, this will
|
|
// match the id passed to ExecuteCharDevUnixSocketAdd. It must be a valid QMP id.
|
|
func (q *QMP) ExecuteChardevDel(ctx context.Context, chardevID string) error {
|
|
args := map[string]interface{}{
|
|
"id": chardevID,
|
|
}
|
|
|
|
return q.executeCommand(ctx, "chardev-remove", args, nil)
|
|
}
|
|
|
|
// ExecuteNetdevAdd adds a Net device to a QEMU instance
|
|
// using the netdev_add command. netdevID is the id of the device to add.
|
|
// Must be valid QMP identifier.
|
|
func (q *QMP) ExecuteNetdevAdd(ctx context.Context, netdevType, netdevID, ifname, downscript, script string, queues int) error {
|
|
args := map[string]interface{}{
|
|
"type": netdevType,
|
|
"id": netdevID,
|
|
"ifname": ifname,
|
|
"downscript": downscript,
|
|
"script": script,
|
|
}
|
|
if queues > 1 {
|
|
args["queues"] = queues
|
|
}
|
|
|
|
return q.executeCommand(ctx, "netdev_add", args, nil)
|
|
}
|
|
|
|
// ExecuteNetdevChardevAdd adds a Net device to a QEMU instance
|
|
// using the netdev_add command. netdevID is the id of the device to add.
|
|
// Must be valid QMP identifier.
|
|
func (q *QMP) ExecuteNetdevChardevAdd(ctx context.Context, netdevType, netdevID, chardev string, queues int) error {
|
|
args := map[string]interface{}{
|
|
"type": netdevType,
|
|
"id": netdevID,
|
|
"chardev": chardev,
|
|
}
|
|
if queues > 1 {
|
|
args["queues"] = queues
|
|
}
|
|
|
|
return q.executeCommand(ctx, "netdev_add", args, nil)
|
|
}
|
|
|
|
// ExecuteNetdevAddByFds adds a Net device to a QEMU instance
|
|
// using the netdev_add command by fds and vhostfds. netdevID is the id of the device to add.
|
|
// Must be valid QMP identifier.
|
|
func (q *QMP) ExecuteNetdevAddByFds(ctx context.Context, netdevType, netdevID string, fdNames, vhostFdNames []string) error {
|
|
fdNameStr := strings.Join(fdNames, ":")
|
|
args := map[string]interface{}{
|
|
"type": netdevType,
|
|
"id": netdevID,
|
|
"fds": fdNameStr,
|
|
}
|
|
if len(vhostFdNames) > 0 {
|
|
vhostFdNameStr := strings.Join(vhostFdNames, ":")
|
|
args["vhost"] = true
|
|
args["vhostfds"] = vhostFdNameStr
|
|
}
|
|
|
|
return q.executeCommand(ctx, "netdev_add", args, nil)
|
|
}
|
|
|
|
// ExecuteNetdevDel deletes a Net device from a QEMU instance
|
|
// using the netdev_del command. netdevID is the id of the device to delete.
|
|
func (q *QMP) ExecuteNetdevDel(ctx context.Context, netdevID string) error {
|
|
args := map[string]interface{}{
|
|
"id": netdevID,
|
|
}
|
|
return q.executeCommand(ctx, "netdev_del", args, nil)
|
|
}
|
|
|
|
// ExecuteNetPCIDeviceAdd adds a Net PCI device to a QEMU instance
|
|
// using the device_add command. devID is the id of the device to add.
|
|
// Must be valid QMP identifier. netdevID is the id of nic added by previous netdev_add.
|
|
// queues is the number of queues of a nic.
|
|
// disableModern indicates if virtio version 1.0 should be replaced by the
|
|
// former version 0.9, as there is a KVM bug that occurs when using virtio
|
|
// 1.0 in nested environments.
|
|
func (q *QMP) ExecuteNetPCIDeviceAdd(ctx context.Context, netdevID, devID, macAddr, addr, bus, romfile string, queues int, disableModern bool) error {
|
|
args := map[string]interface{}{
|
|
"id": devID,
|
|
"driver": VirtioNetPCI,
|
|
"romfile": romfile,
|
|
}
|
|
|
|
if bus != "" {
|
|
args["bus"] = bus
|
|
}
|
|
if addr != "" {
|
|
args["addr"] = addr
|
|
}
|
|
if macAddr != "" {
|
|
args["mac"] = macAddr
|
|
}
|
|
if netdevID != "" {
|
|
args["netdev"] = netdevID
|
|
}
|
|
if disableModern {
|
|
args["disable-modern"] = disableModern
|
|
}
|
|
|
|
if queues > 0 {
|
|
// (2N+2 vectors, N for tx queues, N for rx queues, 1 for config, and one for possible control vq)
|
|
// -device virtio-net-pci,mq=on,vectors=2N+2...
|
|
// enable mq in guest by 'ethtool -L eth0 combined $queue_num'
|
|
// Clearlinux automatically sets up the queues properly
|
|
// The agent implementation should do this to ensure that it is
|
|
// always set
|
|
args["mq"] = "on"
|
|
args["vectors"] = 2*queues + 2
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteNetCCWDeviceAdd adds a Net CCW device to a QEMU instance
|
|
// using the device_add command. devID is the id of the device to add.
|
|
// Must be valid QMP identifier. netdevID is the id of nic added by previous netdev_add.
|
|
// queues is the number of queues of a nic.
|
|
func (q *QMP) ExecuteNetCCWDeviceAdd(ctx context.Context, netdevID, devID, macAddr, bus string, queues int) error {
|
|
args := map[string]interface{}{
|
|
"id": devID,
|
|
"driver": VirtioNetCCW,
|
|
"netdev": netdevID,
|
|
"mac": macAddr,
|
|
"devno": bus,
|
|
}
|
|
|
|
if queues > 0 {
|
|
args["mq"] = "on"
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteDeviceDel deletes guest portion of a QEMU device by sending a
|
|
// device_del command. devId is the identifier of the device to delete.
|
|
// Typically it would match the devID parameter passed to an earlier call
|
|
// to ExecuteDeviceAdd. It must be a valid QMP identidier.
|
|
//
|
|
// This method blocks until a DEVICE_DELETED event is received for devID.
|
|
func (q *QMP) ExecuteDeviceDel(ctx context.Context, devID string) error {
|
|
args := map[string]interface{}{
|
|
"id": devID,
|
|
}
|
|
filter := &qmpEventFilter{
|
|
eventName: "DEVICE_DELETED",
|
|
dataKey: "device",
|
|
dataValue: devID,
|
|
}
|
|
return q.executeCommand(ctx, "device_del", args, filter)
|
|
}
|
|
|
|
// ExecutePCIDeviceAdd is the PCI version of ExecuteDeviceAdd. This function can be used
|
|
// to hot plug PCI devices on PCI(E) bridges, unlike ExecuteDeviceAdd this function receive the
|
|
// device address on its parent bus. bus is optional. queues specifies the number of queues of
|
|
// a block device. shared denotes if the drive can be shared allowing it to be passed more than once.
|
|
// disableModern indicates if virtio version 1.0 should be replaced by the
|
|
// former version 0.9, as there is a KVM bug that occurs when using virtio
|
|
// 1.0 in nested environments.
|
|
func (q *QMP) ExecutePCIDeviceAdd(ctx context.Context, blockdevID, devID, driver, addr, bus, romfile string, queues int, shared, disableModern bool) error {
|
|
args := map[string]interface{}{
|
|
"id": devID,
|
|
"driver": driver,
|
|
"drive": blockdevID,
|
|
"addr": addr,
|
|
}
|
|
if bus != "" {
|
|
args["bus"] = bus
|
|
}
|
|
if shared {
|
|
args["share-rw"] = "on"
|
|
}
|
|
if queues > 0 {
|
|
args["num-queues"] = strconv.Itoa(queues)
|
|
}
|
|
|
|
var transport VirtioTransport
|
|
|
|
if transport.isVirtioPCI(nil) {
|
|
args["romfile"] = romfile
|
|
|
|
if disableModern {
|
|
args["disable-modern"] = disableModern
|
|
}
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecutePCIVhostUserDevAdd adds a vhost-user device to a QEMU instance using the device_add command.
|
|
// This function can be used to hot plug vhost-user devices on PCI(E) bridges.
|
|
// It receives the bus and the device address on its parent bus. bus is optional.
|
|
// devID is the id of the device to add.Must be valid QMP identifier. chardevID
|
|
// is the QMP identifier of character device using a unix socket as backend.
|
|
// driver is the name of vhost-user driver, like vhost-user-blk-pci.
|
|
func (q *QMP) ExecutePCIVhostUserDevAdd(ctx context.Context, driver, devID, chardevID, addr, bus string) error {
|
|
args := map[string]interface{}{
|
|
"driver": driver,
|
|
"id": devID,
|
|
"chardev": chardevID,
|
|
"addr": addr,
|
|
}
|
|
|
|
if bus != "" {
|
|
args["bus"] = bus
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteVFIODeviceAdd adds a VFIO device to a QEMU instance using the device_add command.
|
|
// devID is the id of the device to add. Must be valid QMP identifier.
|
|
// bdf is the PCI bus-device-function of the pci device.
|
|
// bus is optional. When hot plugging a PCIe device, the bus can be the ID of the pcie-root-port.
|
|
func (q *QMP) ExecuteVFIODeviceAdd(ctx context.Context, devID, bdf, bus, romfile string) error {
|
|
var driver string
|
|
var transport VirtioTransport
|
|
|
|
if transport.isVirtioCCW(nil) {
|
|
driver = string(VfioCCW)
|
|
} else {
|
|
driver = string(VfioPCI)
|
|
}
|
|
|
|
args := map[string]interface{}{
|
|
"id": devID,
|
|
"driver": driver,
|
|
"host": bdf,
|
|
"romfile": romfile,
|
|
}
|
|
if bus != "" {
|
|
args["bus"] = bus
|
|
}
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecutePCIVFIODeviceAdd adds a VFIO device to a QEMU instance using the device_add command.
|
|
// This function can be used to hot plug VFIO devices on PCI(E) bridges, unlike
|
|
// ExecuteVFIODeviceAdd this function receives the bus and the device address on its parent bus.
|
|
// bus is optional. devID is the id of the device to add.Must be valid QMP identifier. bdf is the
|
|
// PCI bus-device-function of the pci device.
|
|
func (q *QMP) ExecutePCIVFIODeviceAdd(ctx context.Context, devID, bdf, addr, bus, romfile string) error {
|
|
args := map[string]interface{}{
|
|
"id": devID,
|
|
"driver": VfioPCI,
|
|
"host": bdf,
|
|
"addr": addr,
|
|
"romfile": romfile,
|
|
}
|
|
|
|
if bus != "" {
|
|
args["bus"] = bus
|
|
}
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecutePCIVFIOMediatedDeviceAdd adds a VFIO mediated device to a QEMU instance using the device_add command.
|
|
// This function can be used to hot plug VFIO mediated devices on PCI(E) bridges or root bus, unlike
|
|
// ExecuteVFIODeviceAdd this function receives the bus and the device address on its parent bus.
|
|
// devID is the id of the device to add. Must be valid QMP identifier. sysfsdev is the VFIO mediated device.
|
|
// Both bus and addr are optional. If they are both set to be empty, the system will pick up an empty slot on root bus.
|
|
func (q *QMP) ExecutePCIVFIOMediatedDeviceAdd(ctx context.Context, devID, sysfsdev, addr, bus, romfile string) error {
|
|
args := map[string]interface{}{
|
|
"id": devID,
|
|
"driver": VfioPCI,
|
|
"sysfsdev": sysfsdev,
|
|
"romfile": romfile,
|
|
}
|
|
|
|
if bus != "" {
|
|
args["bus"] = bus
|
|
}
|
|
if addr != "" {
|
|
args["addr"] = addr
|
|
}
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteAPVFIOMediatedDeviceAdd adds a VFIO mediated AP device to a QEMU instance using the device_add command.
|
|
func (q *QMP) ExecuteAPVFIOMediatedDeviceAdd(ctx context.Context, sysfsdev string) error {
|
|
args := map[string]interface{}{
|
|
"driver": VfioAP,
|
|
"sysfsdev": sysfsdev,
|
|
}
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// isSocketIDSupported returns if the cpu driver supports the socket id option
|
|
func isSocketIDSupported(driver string) bool {
|
|
if driver == "host-s390x-cpu" || driver == "host-powerpc64-cpu" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isThreadIDSupported returns if the cpu driver supports the thread id option
|
|
func isThreadIDSupported(driver string) bool {
|
|
if driver == "host-s390x-cpu" || driver == "host-powerpc64-cpu" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isDieIDSupported returns if the cpu driver and the qemu version support the die id option
|
|
func (q *QMP) isDieIDSupported(driver string) bool {
|
|
return driver == "host-x86_64-cpu"
|
|
}
|
|
|
|
// ExecuteCPUDeviceAdd adds a CPU to a QEMU instance using the device_add command.
|
|
// driver is the CPU model, cpuID must be a unique ID to identify the CPU, socketID is the socket number within
|
|
// node/board the CPU belongs to, coreID is the core number within socket the CPU belongs to, threadID is the
|
|
// thread number within core the CPU belongs to. Note that socketID and threadID are not a requirement for
|
|
// architecures like ppc64le.
|
|
func (q *QMP) ExecuteCPUDeviceAdd(ctx context.Context, driver, cpuID, socketID, dieID, coreID, threadID, romfile string) error {
|
|
args := map[string]interface{}{
|
|
"driver": driver,
|
|
"id": cpuID,
|
|
"core-id": coreID,
|
|
}
|
|
|
|
if socketID != "" && isSocketIDSupported(driver) {
|
|
args["socket-id"] = socketID
|
|
}
|
|
|
|
if threadID != "" && isThreadIDSupported(driver) {
|
|
args["thread-id"] = threadID
|
|
}
|
|
|
|
if q.isDieIDSupported(driver) {
|
|
if dieID != "" {
|
|
args["die-id"] = dieID
|
|
}
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteQueryHotpluggableCPUs returns a slice with the list of hotpluggable CPUs
|
|
func (q *QMP) ExecuteQueryHotpluggableCPUs(ctx context.Context) ([]HotpluggableCPU, error) {
|
|
response, err := q.executeCommandWithResponse(ctx, "query-hotpluggable-cpus", nil, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// convert response to json
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to extract CPU information: %v", err)
|
|
}
|
|
|
|
var cpus []HotpluggableCPU
|
|
// convert json to []HotpluggableCPU
|
|
if err = json.Unmarshal(data, &cpus); err != nil {
|
|
return nil, fmt.Errorf("unable to convert json to hotpluggable CPU: %v", err)
|
|
}
|
|
|
|
return cpus, nil
|
|
}
|
|
|
|
// ExecSetMigrationCaps sets migration capabilities
|
|
func (q *QMP) ExecSetMigrationCaps(ctx context.Context, caps []map[string]interface{}) error {
|
|
args := map[string]interface{}{
|
|
"capabilities": caps,
|
|
}
|
|
|
|
return q.executeCommand(ctx, "migrate-set-capabilities", args, nil)
|
|
}
|
|
|
|
// ExecSetMigrateArguments sets the command line used for migration
|
|
func (q *QMP) ExecSetMigrateArguments(ctx context.Context, url string) error {
|
|
args := map[string]interface{}{
|
|
"uri": url,
|
|
}
|
|
|
|
return q.executeCommand(ctx, "migrate", args, nil)
|
|
}
|
|
|
|
// ExecQueryMemoryDevices returns a slice with the list of memory devices
|
|
func (q *QMP) ExecQueryMemoryDevices(ctx context.Context) ([]MemoryDevices, error) {
|
|
response, err := q.executeCommandWithResponse(ctx, "query-memory-devices", nil, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// convert response to json
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to extract memory devices information: %v", err)
|
|
}
|
|
|
|
var memoryDevices []MemoryDevices
|
|
// convert json to []MemoryDevices
|
|
if err = json.Unmarshal(data, &memoryDevices); err != nil {
|
|
return nil, fmt.Errorf("unable to convert json to memory devices: %v", err)
|
|
}
|
|
|
|
return memoryDevices, nil
|
|
}
|
|
|
|
// ExecQueryCpus returns a slice with the list of `CpuInfo`
|
|
// Since qemu 2.12, we have `query-cpus-fast` as a better choice in production
|
|
// we can still choose `ExecQueryCpus` for compatibility though not recommended.
|
|
func (q *QMP) ExecQueryCpus(ctx context.Context) ([]CPUInfo, error) {
|
|
response, err := q.executeCommandWithResponse(ctx, "query-cpus", nil, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// convert response to json
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to extract memory devices information: %v", err)
|
|
}
|
|
|
|
var cpuInfo []CPUInfo
|
|
// convert json to []CPUInfo
|
|
if err = json.Unmarshal(data, &cpuInfo); err != nil {
|
|
return nil, fmt.Errorf("unable to convert json to CPUInfo: %v", err)
|
|
}
|
|
|
|
return cpuInfo, nil
|
|
}
|
|
|
|
// ExecQueryCpusFast returns a slice with the list of `CpuInfoFast`
|
|
// This is introduced since 2.12, it does not incur a performance penalty and
|
|
// should be used in production instead of query-cpus.
|
|
func (q *QMP) ExecQueryCpusFast(ctx context.Context) ([]CPUInfoFast, error) {
|
|
response, err := q.executeCommandWithResponse(ctx, "query-cpus-fast", nil, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// convert response to json
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to extract memory devices information: %v", err)
|
|
}
|
|
|
|
var cpuInfoFast []CPUInfoFast
|
|
// convert json to []CPUInfoFast
|
|
if err = json.Unmarshal(data, &cpuInfoFast); err != nil {
|
|
return nil, fmt.Errorf("unable to convert json to CPUInfoFast: %v", err)
|
|
}
|
|
|
|
return cpuInfoFast, nil
|
|
}
|
|
|
|
// ExecMemdevAdd adds size of MiB memory device to the guest
|
|
func (q *QMP) ExecMemdevAdd(ctx context.Context, qomtype, id, mempath string, size int, share bool, driver, driverID, addr, bus string) error {
|
|
args := map[string]interface{}{
|
|
"qom-type": qomtype,
|
|
"id": id,
|
|
"size": uint64(size) << 20,
|
|
}
|
|
if mempath != "" {
|
|
args["mem-path"] = mempath
|
|
}
|
|
if share {
|
|
args["share"] = true
|
|
}
|
|
err := q.executeCommand(ctx, "object-add", args, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
q.cfg.Logger.Errorf("Unable to add memory device %s: %v", id, err)
|
|
err = q.executeCommand(ctx, "object-del", map[string]interface{}{"id": id}, nil)
|
|
if err != nil {
|
|
q.cfg.Logger.Warningf("Unable to clean up memory object %s: %v", id, err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
args = map[string]interface{}{
|
|
"driver": driver,
|
|
"id": driverID,
|
|
"memdev": id,
|
|
}
|
|
|
|
if bus != "" {
|
|
args["bus"] = bus
|
|
}
|
|
if addr != "" {
|
|
args["addr"] = addr
|
|
}
|
|
|
|
err = q.executeCommand(ctx, "device_add", args, nil)
|
|
|
|
return err
|
|
}
|
|
|
|
// ExecHotplugMemory adds size of MiB memory to the guest
|
|
func (q *QMP) ExecHotplugMemory(ctx context.Context, qomtype, id, mempath string, size int, share bool) error {
|
|
return q.ExecMemdevAdd(ctx, qomtype, id, mempath, size, share, "pc-dimm", "dimm"+id, "", "")
|
|
}
|
|
|
|
// ExecuteNVDIMMDeviceAdd adds a block device to a QEMU instance using
|
|
// a NVDIMM driver with the device_add command.
|
|
// id is the id of the device to add. It must be a valid QMP identifier.
|
|
// mempath is the path of the device to add, e.g., /dev/rdb0. size is
|
|
// the data size of the device. pmem is to guarantee the persistence of QEMU writes
|
|
// to the vNVDIMM backend.
|
|
func (q *QMP) ExecuteNVDIMMDeviceAdd(ctx context.Context, id, mempath string, size int64, pmem *bool) error {
|
|
args := map[string]interface{}{
|
|
"qom-type": "memory-backend-file",
|
|
"id": "nvdimmbackmem" + id,
|
|
"mem-path": mempath,
|
|
"size": size,
|
|
"share": true,
|
|
}
|
|
|
|
if pmem != nil {
|
|
args["pmem"] = *pmem
|
|
}
|
|
|
|
err := q.executeCommand(ctx, "object-add", args, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
args = map[string]interface{}{
|
|
"driver": "nvdimm",
|
|
"id": "nvdimm" + id,
|
|
"memdev": "nvdimmbackmem" + id,
|
|
}
|
|
if err = q.executeCommand(ctx, "device_add", args, nil); err != nil {
|
|
q.cfg.Logger.Errorf("Unable to hotplug NVDIMM device: %v", err)
|
|
err2 := q.executeCommand(ctx, "object-del", map[string]interface{}{"id": "nvdimmbackmem" + id}, nil)
|
|
if err2 != nil {
|
|
q.cfg.Logger.Warningf("Unable to clean up memory object: %v", err2)
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// ExecuteBalloon sets the size of the balloon, hence updates the memory
|
|
// allocated for the VM.
|
|
func (q *QMP) ExecuteBalloon(ctx context.Context, bytes uint64) error {
|
|
args := map[string]interface{}{
|
|
"value": bytes,
|
|
}
|
|
return q.executeCommand(ctx, "balloon", args, nil)
|
|
}
|
|
|
|
// ExecutePCIVSockAdd adds a vhost-vsock-pci bus
|
|
// disableModern indicates if virtio version 1.0 should be replaced by the
|
|
// former version 0.9, as there is a KVM bug that occurs when using virtio
|
|
// 1.0 in nested environments.
|
|
func (q *QMP) ExecutePCIVSockAdd(ctx context.Context, id, guestCID, vhostfd, addr, bus, romfile string, disableModern bool) error {
|
|
args := map[string]interface{}{
|
|
"driver": VHostVSockPCI,
|
|
"id": id,
|
|
"guest-cid": guestCID,
|
|
"vhostfd": vhostfd,
|
|
"addr": addr,
|
|
"romfile": romfile,
|
|
}
|
|
|
|
if bus != "" {
|
|
args["bus"] = bus
|
|
}
|
|
|
|
if disableModern {
|
|
args["disable-modern"] = disableModern
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteGetFD sends a file descriptor via SCM rights and assigns it a name
|
|
func (q *QMP) ExecuteGetFD(ctx context.Context, fdname string, fd *os.File) error {
|
|
oob := syscall.UnixRights(int(fd.Fd()))
|
|
args := map[string]interface{}{
|
|
"fdname": fdname,
|
|
}
|
|
|
|
_, err := q.executeCommandWithResponse(ctx, "getfd", args, oob, nil)
|
|
return err
|
|
}
|
|
|
|
// ExecuteCharDevUnixSocketAdd adds a character device using as backend a unix socket,
|
|
// id is an identifier for the device, path specifies the local path of the unix socket,
|
|
// wait is to block waiting for a client to connect, server specifies that the socket is a listening socket.
|
|
func (q *QMP) ExecuteCharDevUnixSocketAdd(ctx context.Context, id, path string, wait, server bool) error {
|
|
data := map[string]interface{}{
|
|
"server": server,
|
|
"addr": map[string]interface{}{
|
|
"type": "unix",
|
|
"data": map[string]interface{}{
|
|
"path": path,
|
|
},
|
|
},
|
|
}
|
|
|
|
// wait is only valid for server mode
|
|
if server {
|
|
data["wait"] = wait
|
|
}
|
|
|
|
args := map[string]interface{}{
|
|
"id": id,
|
|
"backend": map[string]interface{}{
|
|
"type": "socket",
|
|
"data": data,
|
|
},
|
|
}
|
|
return q.executeCommand(ctx, "chardev-add", args, nil)
|
|
}
|
|
|
|
// ExecuteVirtSerialPortAdd adds a virtserialport.
|
|
// id is an identifier for the virtserialport, name is a name for the virtserialport and
|
|
// it will be visible in the VM, chardev is the character device id previously added.
|
|
func (q *QMP) ExecuteVirtSerialPortAdd(ctx context.Context, id, name, chardev string) error {
|
|
args := map[string]interface{}{
|
|
"driver": VirtioSerialPort,
|
|
"id": id,
|
|
"name": name,
|
|
"chardev": chardev,
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteQueryMigration queries migration progress.
|
|
func (q *QMP) ExecuteQueryMigration(ctx context.Context) (MigrationStatus, error) {
|
|
response, err := q.executeCommandWithResponse(ctx, "query-migrate", nil, nil, nil)
|
|
if err != nil {
|
|
return MigrationStatus{}, err
|
|
}
|
|
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
return MigrationStatus{}, fmt.Errorf("unable to extract migrate status information: %v", err)
|
|
}
|
|
|
|
var status MigrationStatus
|
|
if err = json.Unmarshal(data, &status); err != nil {
|
|
return MigrationStatus{}, fmt.Errorf("unable to convert migrate status information: %v", err)
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// ExecuteMigrationIncoming start migration from incoming uri.
|
|
func (q *QMP) ExecuteMigrationIncoming(ctx context.Context, uri string) error {
|
|
args := map[string]interface{}{
|
|
"uri": uri,
|
|
}
|
|
return q.executeCommand(ctx, "migrate-incoming", args, nil)
|
|
}
|
|
|
|
// ExecQueryQmpSchema query all QMP wire ABI and returns a slice
|
|
func (q *QMP) ExecQueryQmpSchema(ctx context.Context) ([]SchemaInfo, error) {
|
|
response, err := q.executeCommandWithResponse(ctx, "query-qmp-schema", nil, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// convert response to json
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to extract memory devices information: %v", err)
|
|
}
|
|
|
|
var schemaInfo []SchemaInfo
|
|
if err = json.Unmarshal(data, &schemaInfo); err != nil {
|
|
return nil, fmt.Errorf("unable to convert json to schemaInfo: %v", err)
|
|
}
|
|
|
|
return schemaInfo, nil
|
|
}
|
|
|
|
// ExecuteQueryStatus queries guest status
|
|
func (q *QMP) ExecuteQueryStatus(ctx context.Context) (StatusInfo, error) {
|
|
response, err := q.executeCommandWithResponse(ctx, "query-status", nil, nil, nil)
|
|
if err != nil {
|
|
return StatusInfo{}, err
|
|
}
|
|
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
return StatusInfo{}, fmt.Errorf("unable to extract migrate status information: %v", err)
|
|
}
|
|
|
|
var status StatusInfo
|
|
if err = json.Unmarshal(data, &status); err != nil {
|
|
return StatusInfo{}, fmt.Errorf("unable to convert migrate status information: %v", err)
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// ExecQomSet qom-set path property value
|
|
func (q *QMP) ExecQomSet(ctx context.Context, path, property string, value uint64) error {
|
|
args := map[string]interface{}{
|
|
"path": path,
|
|
"property": property,
|
|
"value": value,
|
|
}
|
|
|
|
return q.executeCommand(ctx, "qom-set", args, nil)
|
|
}
|
|
|
|
// ExecQomGet qom-get path property
|
|
func (q *QMP) ExecQomGet(ctx context.Context, path, property string) (interface{}, error) {
|
|
args := map[string]interface{}{
|
|
"path": path,
|
|
"property": property,
|
|
}
|
|
|
|
response, err := q.executeCommandWithResponse(ctx, "qom-get", args, nil, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
// ExecuteDumpGuestMemory dump guest memory to host
|
|
func (q *QMP) ExecuteDumpGuestMemory(ctx context.Context, protocol string, paging bool, format string) error {
|
|
args := map[string]interface{}{
|
|
"protocol": protocol,
|
|
"paging": paging,
|
|
"format": format,
|
|
}
|
|
|
|
return q.executeCommand(ctx, "dump-guest-memory", args, nil)
|
|
}
|