mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
Treat query-migrate as a heavyCommandTimeout. That's because just like stop, it can get stuck on QEMU handling I/O internally (final stage of a migration). Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
419 lines
10 KiB
Go
419 lines
10 KiB
Go
package qmp
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"slices"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/lxc/incus/v7/shared/logger"
|
|
)
|
|
|
|
type qemuMachineProtocol struct {
|
|
oobSupported bool // Out of band support or not
|
|
uc *net.UnixConn // Underlying unix socket connection
|
|
mu sync.Mutex // Serialize running command
|
|
replies sync.Map // Replies channels
|
|
events <-chan qmpEvent // Events channel
|
|
listeners atomic.Uint32 // Listeners number
|
|
cid atomic.Uint32 // Auto increase command id
|
|
log *qmpLog // qmp log
|
|
}
|
|
|
|
// qmpEvent represents a QEMU QMP event.
|
|
type qmpEvent struct {
|
|
// Event name, e.g., BLOCK_JOB_COMPLETE
|
|
Event string `json:"event"`
|
|
|
|
// Arbitrary event data
|
|
Data map[string]any `json:"data"`
|
|
|
|
// Event timestamp, provided by QEMU.
|
|
Timestamp *struct {
|
|
Seconds int64 `json:"seconds"`
|
|
Microseconds int64 `json:"microseconds"`
|
|
} `json:"timestamp"`
|
|
}
|
|
|
|
// qmpCommand represents a QMP command.
|
|
type qmpCommand struct {
|
|
// Name of the command to run
|
|
Execute string `json:"execute,omitempty"`
|
|
|
|
// Name of the Out-off-band execution to run
|
|
ExecuteOutOfBand string `json:"exec-oob,omitempty"`
|
|
|
|
// Optional arguments for the above command.
|
|
Arguments any `json:"arguments,omitempty"`
|
|
|
|
// Optional id for transaction identification associated with the command
|
|
// execution
|
|
//
|
|
// According QMP spec it should be any json value type. For incus `uint32`
|
|
// (skip zero) is good enough to identify transaction.
|
|
ID uint32 `json:"id,omitempty"`
|
|
}
|
|
|
|
// qmpResponse represents a QMP response with id and return.
|
|
type qmpResponse struct {
|
|
// Optional id for transaction identification associated with the response
|
|
ID uint32 `json:"id,omitempty"`
|
|
|
|
// Return response return
|
|
Return any `json:"return,omitempty"`
|
|
}
|
|
|
|
// qmpError represents a QMP response error.
|
|
type qmpError struct {
|
|
Class string `json:"class,omitempty"`
|
|
Desc string `json:"desc,omitempty"`
|
|
}
|
|
|
|
func (e *qmpError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
|
|
return fmt.Sprintf("%s: %s", e.Class, e.Desc)
|
|
}
|
|
|
|
// rawResponse represents QMP raw response with id, error and raw bytes.
|
|
type rawResponse struct {
|
|
// Optional id for transaction identification associated with the response
|
|
ID uint32 `json:"id"`
|
|
|
|
// Error response error
|
|
Error *qmpError `json:"error,omitempty"`
|
|
|
|
raw []byte // raw data, json field ignored
|
|
err error // runtime error, json field ignored
|
|
}
|
|
|
|
// disconnect closes the QEMU monitor socket connection.
|
|
func (qmp *qemuMachineProtocol) disconnect() error {
|
|
qmp.listeners.Store(0)
|
|
if qmp.log != nil {
|
|
err := qmp.log.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
qmp.log = nil
|
|
}
|
|
|
|
return qmp.uc.Close()
|
|
}
|
|
|
|
// qmpIncreaseID increase ID and skip zero.
|
|
func (qmp *qemuMachineProtocol) qmpIncreaseID() uint32 {
|
|
id := qmp.cid.Add(1)
|
|
if id == 0 {
|
|
id = qmp.cid.Add(1)
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
// connect sets up a QMP connection.
|
|
func (qmp *qemuMachineProtocol) connect() error {
|
|
enc := json.NewEncoder(qmp.uc)
|
|
dec := json.NewDecoder(qmp.uc)
|
|
|
|
// Check for banner on startup
|
|
ban := struct {
|
|
QMP struct {
|
|
Capabilities []string `json:"capabilities"`
|
|
} `json:"QMP"`
|
|
}{}
|
|
|
|
err := dec.Decode(&ban)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
qmp.oobSupported = slices.Contains(ban.QMP.Capabilities, "oob")
|
|
|
|
// Issue capabilities handshake
|
|
id := qmp.qmpIncreaseID()
|
|
cmd := qmpCommand{Execute: "qmp_capabilities", ID: id}
|
|
err = enc.Encode(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check for no error on return
|
|
r := &rawResponse{}
|
|
err = dec.Decode(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if r.Error != nil {
|
|
return r.Error
|
|
}
|
|
|
|
if r.ID != id {
|
|
return fmt.Errorf("reply id %d and command id %d mismatch", r.ID, id)
|
|
}
|
|
|
|
// Initialize listener for command responses and asynchronous events.
|
|
events := make(chan qmpEvent, 128)
|
|
go qmp.listen(qmp.uc, events, &qmp.replies)
|
|
qmp.events = events
|
|
return nil
|
|
}
|
|
|
|
// getEvents streams QEMU QMP Events.
|
|
func (qmp *qemuMachineProtocol) getEvents(context.Context) (<-chan qmpEvent, error) {
|
|
qmp.listeners.Add(1)
|
|
return qmp.events, nil
|
|
}
|
|
|
|
func (qmp *qemuMachineProtocol) listen(r io.Reader, events chan<- qmpEvent, replies *sync.Map) {
|
|
defer close(events)
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
for scanner.Scan() {
|
|
var e qmpEvent
|
|
|
|
b := scanner.Bytes()
|
|
err := json.Unmarshal(b, &e)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// If data does not have an event type, it must be in response to a command.
|
|
if e.Event == "" {
|
|
r := rawResponse{}
|
|
err = json.Unmarshal(b, &r)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
key := r.ID
|
|
if key == 0 {
|
|
// Discard response without a request ID.
|
|
continue
|
|
}
|
|
|
|
val, ok := replies.LoadAndDelete(key)
|
|
if !ok {
|
|
// Discard unexpected response.
|
|
continue
|
|
}
|
|
|
|
reply, ok := val.(chan rawResponse)
|
|
if !ok {
|
|
// Skip bad messages.
|
|
logger.Error("Failed to cast QMP reply to chan rawResponse")
|
|
continue
|
|
}
|
|
|
|
r.raw = make([]byte, len(b))
|
|
copy(r.raw, b)
|
|
reply <- r
|
|
|
|
continue
|
|
}
|
|
|
|
if qmp.log != nil && !slices.Contains([]string{"VSERPORT_CHANGE"}, e.Event) {
|
|
_, err := fmt.Fprintf(qmp.log, "[%s] Event: %s\n", time.Now().Format(time.RFC3339), b)
|
|
if err != nil {
|
|
logger.Debugf("Failed to log event: %v", err)
|
|
}
|
|
}
|
|
|
|
// If nobody is listening for events, do not bother sending them.
|
|
if qmp.listeners.Load() == 0 {
|
|
continue
|
|
}
|
|
|
|
events <- e
|
|
}
|
|
|
|
err := scanner.Err()
|
|
if err == nil {
|
|
err = errors.New("Monitor has exited")
|
|
}
|
|
|
|
// Return the error to all existing requests.
|
|
replies.Range(func(k any, v any) bool {
|
|
reply, ok := v.(chan rawResponse)
|
|
if !ok {
|
|
// Skip bad messages.
|
|
logger.Error("Failed to cast QMP reply to chan rawResponse")
|
|
|
|
return true
|
|
}
|
|
|
|
reply <- rawResponse{err: err}
|
|
|
|
return true
|
|
})
|
|
|
|
// Clear the map.
|
|
replies.Clear()
|
|
}
|
|
|
|
// defaultCommandTimeout is how long we wait for a QMP reply before giving up.
|
|
const defaultCommandTimeout = 500 * time.Millisecond
|
|
|
|
// blockCommandTimeout is used for block commands that do blocking storage I/O.
|
|
const blockCommandTimeout = 5 * time.Second
|
|
|
|
// heavyCommandTimeout is used for commands that require I/O flush or VFIO operations.
|
|
const heavyCommandTimeout = 30 * time.Second
|
|
|
|
// commandTimeouts overrides defaultCommandTimeout for synchronous slow commands.
|
|
var commandTimeouts = map[string]time.Duration{
|
|
"block-commit": blockCommandTimeout,
|
|
"block-dirty-bitmap-remove": blockCommandTimeout,
|
|
"block-export-add": blockCommandTimeout,
|
|
"block-job-cancel": blockCommandTimeout,
|
|
"block-job-complete": blockCommandTimeout,
|
|
"block_resize": blockCommandTimeout,
|
|
"block_set_io_throttle": blockCommandTimeout,
|
|
"blockdev-mirror": blockCommandTimeout,
|
|
"blockdev-snapshot": blockCommandTimeout,
|
|
"change-backing-file": blockCommandTimeout,
|
|
"job-dismiss": blockCommandTimeout,
|
|
"query-block-jobs": blockCommandTimeout,
|
|
"nbd-server-start": blockCommandTimeout,
|
|
"nbd-server-stop": blockCommandTimeout,
|
|
"transaction": blockCommandTimeout,
|
|
"blockdev-add": heavyCommandTimeout,
|
|
"blockdev-del": heavyCommandTimeout,
|
|
"cont": heavyCommandTimeout,
|
|
"device_add": heavyCommandTimeout,
|
|
"device_del": heavyCommandTimeout,
|
|
"netdev_add": heavyCommandTimeout,
|
|
"netdev_del": heavyCommandTimeout,
|
|
"object-add": heavyCommandTimeout,
|
|
"stop": heavyCommandTimeout,
|
|
"system_reset": heavyCommandTimeout,
|
|
"query-migrate": heavyCommandTimeout,
|
|
"screendump": 10 * time.Second,
|
|
}
|
|
|
|
// commandName extracts the command name from a marshalled QMP request.
|
|
func commandName(command []byte) string {
|
|
var req struct {
|
|
Execute string `json:"execute"`
|
|
ExecuteOutOfBand string `json:"exec-oob"`
|
|
}
|
|
|
|
_ = json.Unmarshal(command, &req)
|
|
if req.Execute != "" {
|
|
return req.Execute
|
|
}
|
|
|
|
return req.ExecuteOutOfBand
|
|
}
|
|
|
|
// run executes the given QAPI command against a domain's QEMU instance.
|
|
func (qmp *qemuMachineProtocol) run(command []byte, id uint32) ([]byte, error) {
|
|
// Just call RunWithFile with no file
|
|
return qmp.runWithFile(command, nil, id)
|
|
}
|
|
|
|
func (qmp *qemuMachineProtocol) qmpWriteMsg(b []byte, file *os.File) error {
|
|
if file == nil {
|
|
// Just send a normal command through.
|
|
_, err := qmp.uc.Write(b)
|
|
return err
|
|
}
|
|
|
|
if !qmp.oobSupported {
|
|
return errors.New("The QEMU server doesn't support oob (needed for RunWithFile)")
|
|
}
|
|
|
|
// Send the command along with the file descriptor.
|
|
oob := unix.UnixRights(int(file.Fd()))
|
|
_, _, err := qmp.uc.WriteMsgUnix(b, oob, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runWithFile executes for passing a file through out-of-band data.
|
|
func (qmp *qemuMachineProtocol) runWithFile(command []byte, file *os.File, id uint32) ([]byte, error) {
|
|
// Only allow a single command to be run at a time to ensure that responses
|
|
// to a command cannot be mixed with responses from another command
|
|
qmp.mu.Lock()
|
|
defer qmp.mu.Unlock()
|
|
|
|
if id == 0 {
|
|
id = qmp.qmpIncreaseID()
|
|
b, err := qmp.qmpInjectID(command, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
command = b
|
|
}
|
|
|
|
repCh := make(chan rawResponse, 1)
|
|
qmp.replies.Store(id, repCh)
|
|
|
|
err := qmp.qmpWriteMsg(command, file)
|
|
if err != nil {
|
|
qmp.replies.Delete(id)
|
|
return nil, err
|
|
}
|
|
|
|
// Pick the timeout for this command.
|
|
cmd := commandName(command)
|
|
timeout := defaultCommandTimeout
|
|
override, ok := commandTimeouts[cmd]
|
|
if ok {
|
|
timeout = override
|
|
}
|
|
|
|
// Wait for a response, error or timeout.
|
|
var res rawResponse
|
|
select {
|
|
case res = <-repCh:
|
|
case <-time.After(timeout):
|
|
qmp.replies.Delete(id)
|
|
return nil, fmt.Errorf("%w: %q after %s", ErrMonitorTimeout, cmd, timeout)
|
|
}
|
|
|
|
if res.err != nil {
|
|
return nil, res.err
|
|
}
|
|
|
|
if res.Error != nil {
|
|
return nil, res.Error
|
|
}
|
|
|
|
return res.raw, nil
|
|
}
|
|
|
|
func (qmp *qemuMachineProtocol) qmpInjectID(command []byte, id uint32) ([]byte, error) {
|
|
req := &qmpCommand{}
|
|
err := json.Unmarshal(command, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.ID = id
|
|
b, err := json.Marshal(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b, nil
|
|
}
|