mirror of
https://github.com/aljazceru/kata-containers.git
synced 2025-12-18 23:04:20 +01:00
Currently, virtio transports for each device are determined with architecture dependent build time conditionals. This isn't the ideal solution, as virtio transports aren't exactly tied to the host's architecture. For example, aarch64 VMs do support both PCI and MMIO devices, and after the recent introduction of the microvm machine type, that's also the case for x86_64. This patch extends each device that supports multiple transports with a VirtioTransport field, so users of the library can manually specify a transport for each device. To avoid breaking the compatibility, if VirtioTransport is empty a behavior equivalent to the legacy one is achieved by checking runtime.GOARCH and Config.Machine.Type. Keeping support for isVirtioPCI/isVirtioCCW in qmp.go is a bit tricky. Eventually, the hot-plug API should be extended so callers must manually specify the transport for the device. Signed-off-by: Sergio Lopez <slp@redhat.com>
1624 lines
51 KiB
Go
1624 lines
51 KiB
Go
/*
|
|
// Copyright contributors to the Virtual Machine Manager for Go project
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
*/
|
|
|
|
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.
|
|
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
|
|
}
|
|
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
type MemoryDevices struct {
|
|
Data MemoryDevicesData `json:"data"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// CPUInfo represents information about each virtual CPU
|
|
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
|
|
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
|
|
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()
|
|
if q.cfg.Logger.V(1) {
|
|
q.cfg.Logger.Infof("%s", string(line))
|
|
}
|
|
|
|
// 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("sanner 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)
|
|
}
|
|
q.cfg.Logger.Infof("%s", string(encodedCmd))
|
|
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")
|
|
}
|
|
}
|
|
|
|
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: "SHUTDOWN",
|
|
}
|
|
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(device, blockdevID string) (map[string]interface{}, map[string]interface{}) {
|
|
var args map[string]interface{}
|
|
|
|
blockdevArgs := map[string]interface{}{
|
|
"driver": "raw",
|
|
"file": map[string]interface{}{
|
|
"driver": "file",
|
|
"filename": device,
|
|
},
|
|
}
|
|
|
|
if q.version.Major > 2 || (q.version.Major == 2 && q.version.Minor >= 8) {
|
|
blockdevArgs["node-name"] = blockdevID
|
|
args = blockdevArgs
|
|
} else {
|
|
blockdevArgs["id"] = blockdevID
|
|
args = map[string]interface{}{
|
|
"options": 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) error {
|
|
args, _ := q.blockdevAddBaseArgs(device, blockdevID)
|
|
|
|
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 bool) error {
|
|
args, blockdevArgs := q.blockdevAddBaseArgs(device, blockdevID)
|
|
|
|
if q.version.Major < 2 || (q.version.Major == 2 && q.version.Minor < 9) {
|
|
return fmt.Errorf("versions of qemu (%d.%d) older than 2.9 do not support set cache-related options for block devices",
|
|
q.version.Major, q.version.Minor)
|
|
}
|
|
|
|
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 && (q.version.Major > 2 || (q.version.Major == 2 && q.version.Minor >= 10)) {
|
|
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 && (q.version.Major > 2 || (q.version.Major == 2 && q.version.Minor >= 10)) {
|
|
args["share-rw"] = "on"
|
|
}
|
|
|
|
return q.executeCommand(ctx, "device_add", args, nil)
|
|
}
|
|
|
|
// ExecuteBlockdevDel deletes a block device by sending a x-blockdev-del command
|
|
// for qemu versions < 2.9. It sends the updated blockdev-del command for qemu>=2.9.
|
|
// 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{}{}
|
|
|
|
if q.version.Major > 2 || (q.version.Major == 2 && q.version.Minor >= 9) {
|
|
args["node-name"] = blockdevID
|
|
return q.executeCommand(ctx, "blockdev-del", args, nil)
|
|
}
|
|
|
|
if q.version.Major == 2 && q.version.Minor == 8 {
|
|
args["node-name"] = blockdevID
|
|
} else {
|
|
args["id"] = blockdevID
|
|
}
|
|
|
|
return q.executeCommand(ctx, "x-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"] = "on"
|
|
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 && (q.version.Major > 2 || (q.version.Major == 2 && q.version.Minor >= 10)) {
|
|
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)
|
|
}
|
|
|
|
// 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 {
|
|
if (q.version.Major > 4 || (q.version.Major == 4 && q.version.Minor >= 1)) && driver == "host-x86_64-cpu" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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 string) error {
|
|
props := map[string]interface{}{"size": uint64(size) << 20}
|
|
args := map[string]interface{}{
|
|
"qom-type": qomtype,
|
|
"id": id,
|
|
"props": props,
|
|
}
|
|
if mempath != "" {
|
|
props["mem-path"] = mempath
|
|
}
|
|
if share {
|
|
props["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,
|
|
}
|
|
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.
|
|
func (q *QMP) ExecuteNVDIMMDeviceAdd(ctx context.Context, id, mempath string, size int64) error {
|
|
args := map[string]interface{}{
|
|
"qom-type": "memory-backend-file",
|
|
"id": "nvdimmbackmem" + id,
|
|
"props": map[string]interface{}{
|
|
"mem-path": mempath,
|
|
"size": size,
|
|
"share": true,
|
|
},
|
|
}
|
|
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 {
|
|
args := map[string]interface{}{
|
|
"id": id,
|
|
"backend": map[string]interface{}{
|
|
"type": "socket",
|
|
"data": map[string]interface{}{
|
|
"wait": wait,
|
|
"server": server,
|
|
"addr": map[string]interface{}{
|
|
"type": "unix",
|
|
"data": map[string]interface{}{
|
|
"path": path,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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)
|
|
}
|