qvm/internal/qmp/client.go

160 lines
4.5 KiB
Go

package qmp
import (
"encoding/json"
"fmt"
"time"
"github.com/digitalocean/go-qemu/qmp"
"github.com/samber/mo"
)
type Client struct {
monitor qmp.Monitor
}
type VMStatus struct {
Running bool
Singlestep bool
Status string
}
func Connect(socketPath string) mo.Result[*Client] {
monitor, err := qmp.NewSocketMonitor("unix", socketPath, 2*time.Second)
if err != nil {
return mo.Err[*Client](fmt.Errorf("failed to create socket monitor: %w", err))
}
if err := monitor.Connect(); err != nil {
return mo.Err[*Client](fmt.Errorf("failed to connect to QMP socket: %w", err))
}
return mo.Ok(&Client{monitor: monitor})
}
func (c *Client) Status() mo.Result[VMStatus] {
type statusResult struct {
ID string `json:"id"`
Return struct {
Running bool `json:"running"`
Singlestep bool `json:"singlestep"`
Status string `json:"status"`
} `json:"return"`
}
cmd := []byte(`{"execute":"query-status"}`)
raw, err := c.monitor.Run(cmd)
if err != nil {
return mo.Err[VMStatus](fmt.Errorf("failed to execute query-status: %w", err))
}
var result statusResult
if err := json.Unmarshal(raw, &result); err != nil {
return mo.Err[VMStatus](fmt.Errorf("failed to parse status response: %w", err))
}
return mo.Ok(VMStatus{
Running: result.Return.Running,
Singlestep: result.Return.Singlestep,
Status: result.Return.Status,
})
}
func (c *Client) Shutdown() mo.Result[struct{}] {
cmd := []byte(`{"execute":"system_powerdown"}`)
_, err := c.monitor.Run(cmd)
if err != nil {
return mo.Err[struct{}](fmt.Errorf("failed to execute system_powerdown: %w", err))
}
return mo.Ok(struct{}{})
}
func (c *Client) Close() error {
return c.monitor.Disconnect()
}
// AddChardev adds a vhost-user-fs chardev for a virtiofsd socket.
// This is the first step of hot-mounting a filesystem.
func (c *Client) AddChardev(id, socketPath string) mo.Result[struct{}] {
cmd := fmt.Sprintf(`{"execute":"chardev-add","arguments":{"id":"%s","backend":{"type":"socket","data":{"addr":{"type":"unix","data":{"path":"%s"}},"server":false}}}}`, id, socketPath)
raw, err := c.monitor.Run([]byte(cmd))
if err != nil {
return mo.Err[struct{}](fmt.Errorf("failed to add chardev %s: %w", id, err))
}
// Check for error in response
var resp map[string]interface{}
if err := json.Unmarshal(raw, &resp); err == nil {
if errObj, ok := resp["error"]; ok {
return mo.Err[struct{}](fmt.Errorf("QMP error adding chardev: %v", errObj))
}
}
return mo.Ok(struct{}{})
}
// AddVhostUserFsDevice adds a vhost-user-fs-pci device connected to a chardev.
// This is the second step of hot-mounting a filesystem.
func (c *Client) AddVhostUserFsDevice(chardevID, tag string) mo.Result[struct{}] {
// Device ID must be unique, derive from chardev ID
deviceID := "dev_" + chardevID
cmd := fmt.Sprintf(`{"execute":"device_add","arguments":{"driver":"vhost-user-fs-pci","chardev":"%s","tag":"%s","id":"%s","queue-size":1024}}`, chardevID, tag, deviceID)
raw, err := c.monitor.Run([]byte(cmd))
if err != nil {
return mo.Err[struct{}](fmt.Errorf("failed to add vhost-user-fs device for %s: %w", tag, err))
}
// Check for error in response
var resp map[string]interface{}
if err := json.Unmarshal(raw, &resp); err == nil {
if errObj, ok := resp["error"]; ok {
return mo.Err[struct{}](fmt.Errorf("QMP error adding device: %v", errObj))
}
}
return mo.Ok(struct{}{})
}
// HotMountFilesystem performs a complete hot-mount of a virtiofsd filesystem.
// Requires the virtiofsd daemon to already be running and listening on socketPath.
func (c *Client) HotMountFilesystem(tag, socketPath string) mo.Result[struct{}] {
// Use tag as chardev ID for simplicity
chardevID := tag
// Step 1: Add chardev
if result := c.AddChardev(chardevID, socketPath); result.IsError() {
return result
}
// Step 2: Add device
if result := c.AddVhostUserFsDevice(chardevID, tag); result.IsError() {
return result
}
return mo.Ok(struct{}{})
}
// QueryChardevs lists all current chardevs to check if one exists.
func (c *Client) QueryChardevs() mo.Result[[]string] {
cmd := []byte(`{"execute":"query-chardev"}`)
raw, err := c.monitor.Run(cmd)
if err != nil {
return mo.Err[[]string](fmt.Errorf("failed to query chardevs: %w", err))
}
var resp struct {
Return []struct {
Label string `json:"label"`
} `json:"return"`
}
if err := json.Unmarshal(raw, &resp); err != nil {
return mo.Err[[]string](fmt.Errorf("failed to parse chardev response: %w", err))
}
var labels []string
for _, c := range resp.Return {
labels = append(labels, c.Label)
}
return mo.Ok(labels)
}