incus-mirror/internal/server/instance/drivers/driver_qemu.go
Stéphane Graber 7dbbb0fc9b
incusd/instance/qemu: Use copy-before-write overlays for NBD exports
Closes #3741

Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
2026-07-28 22:53:34 -04:00

12434 lines
351 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package drivers
import (
"bufio"
"bytes"
"compress/gzip"
"context"
"crypto/tls"
"crypto/x509"
"database/sql"
"embed"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"maps"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"sync"
"time"
"unsafe"
"github.com/flosch/pongo2/v6"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/kballard/go-shellquote"
"github.com/mdlayher/vsock"
"github.com/pkg/sftp"
"go.yaml.in/yaml/v4"
"golang.org/x/sync/errgroup"
"golang.org/x/sys/unix"
"google.golang.org/protobuf/proto"
incus "github.com/lxc/incus/v7/client"
internalInstance "github.com/lxc/incus/v7/internal/instance"
"github.com/lxc/incus/v7/internal/instancewriter"
"github.com/lxc/incus/v7/internal/jmap"
"github.com/lxc/incus/v7/internal/linux"
"github.com/lxc/incus/v7/internal/migration"
"github.com/lxc/incus/v7/internal/ports"
"github.com/lxc/incus/v7/internal/server/apparmor"
"github.com/lxc/incus/v7/internal/server/cgroup"
"github.com/lxc/incus/v7/internal/server/db"
dbCluster "github.com/lxc/incus/v7/internal/server/db/cluster"
"github.com/lxc/incus/v7/internal/server/device"
deviceConfig "github.com/lxc/incus/v7/internal/server/device/config"
"github.com/lxc/incus/v7/internal/server/device/nictype"
"github.com/lxc/incus/v7/internal/server/instance"
"github.com/lxc/incus/v7/internal/server/instance/drivers/cfg"
"github.com/lxc/incus/v7/internal/server/instance/drivers/edk2"
"github.com/lxc/incus/v7/internal/server/instance/drivers/qemudefault"
"github.com/lxc/incus/v7/internal/server/instance/drivers/qmp"
"github.com/lxc/incus/v7/internal/server/instance/instancetype"
"github.com/lxc/incus/v7/internal/server/instance/operationlock"
"github.com/lxc/incus/v7/internal/server/lifecycle"
"github.com/lxc/incus/v7/internal/server/metrics"
localMigration "github.com/lxc/incus/v7/internal/server/migration"
"github.com/lxc/incus/v7/internal/server/network"
"github.com/lxc/incus/v7/internal/server/operations"
"github.com/lxc/incus/v7/internal/server/project"
"github.com/lxc/incus/v7/internal/server/response"
"github.com/lxc/incus/v7/internal/server/scriptlet"
scriptletLoad "github.com/lxc/incus/v7/internal/server/scriptlet/load"
"github.com/lxc/incus/v7/internal/server/selinux"
"github.com/lxc/incus/v7/internal/server/state"
storagePools "github.com/lxc/incus/v7/internal/server/storage"
storageDrivers "github.com/lxc/incus/v7/internal/server/storage/drivers"
localUtil "github.com/lxc/incus/v7/internal/server/util"
localvsock "github.com/lxc/incus/v7/internal/server/vsock"
internalUtil "github.com/lxc/incus/v7/internal/util"
"github.com/lxc/incus/v7/internal/version"
"github.com/lxc/incus/v7/shared/api"
agentAPI "github.com/lxc/incus/v7/shared/api/agent"
"github.com/lxc/incus/v7/shared/ioprogress"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/osarch"
"github.com/lxc/incus/v7/shared/osinfo"
"github.com/lxc/incus/v7/shared/resources"
"github.com/lxc/incus/v7/shared/revert"
"github.com/lxc/incus/v7/shared/subprocess"
localtls "github.com/lxc/incus/v7/shared/tls"
"github.com/lxc/incus/v7/shared/uefi"
"github.com/lxc/incus/v7/shared/units"
"github.com/lxc/incus/v7/shared/util"
)
// incus-agent files
//
//go:embed agent-loader/*
var incusAgentLoader embed.FS
// qemuSerialChardevName is used to communicate state with QEMU via QMP.
const qemuSerialChardevName = "qemu_serial-chardev"
// qemuDeviceIDPrefix used as part of the name given QEMU devices generated from user added devices.
const qemuDeviceIDPrefix = "dev-incus_"
// qemuNetDevIDPrefix used as part of the name given QEMU netdevs generated from user added devices.
const qemuNetDevIDPrefix = "incus_"
// qemuBlockDevIDPrefix used as part of the name given QEMU blockdevs generated from user added devices.
const qemuBlockDevIDPrefix = "incus_"
// qemuMountTagMaxLength defines the maximum number of characters allowed for the mount tag added to the QEMU configuration.
const qemuMountTagMaxLength = 30
// qemuMountTag9pMaxLength defines the maximum number of characters allowed for the mount tag added to the QEMU configuration when using 9p.
const qemuMountTag9pMaxLength = 25
// qemuMountTagPrefix is the prefix used for QEMU mount tags for directory shares.
const qemuMountTagPrefix = "incus_"
// qemuSparseUSBPorts is the amount of sparse USB ports for VMs.
// 4 are reserved, and the other 4 can be used for any USB device.
const qemuSparseUSBPorts = 8
var errQemuAgentOffline = errors.New("VM agent isn't currently running")
type monitorHook func(m *qmp.Monitor) error
// qemuLoad creates a Qemu instance from the supplied InstanceArgs.
func qemuLoad(s *state.State, args db.InstanceArgs, p api.Project) (instance.Instance, error) {
// Create the instance struct.
d := qemuInstantiate(s, args, nil, p)
// Expand config and devices.
err := d.expandConfig()
if err != nil {
return nil, err
}
return d, nil
}
// qemuInstantiate creates a Qemu struct without expanding config. The expandedDevices argument is
// used during device config validation when the devices have already been expanded and we do not
// have access to the profiles used to do it. This can be safely passed as nil if not required.
func qemuInstantiate(s *state.State, args db.InstanceArgs, expandedDevices deviceConfig.Devices, p api.Project) *qemu {
d := &qemu{
common: common{
state: s,
architecture: args.Architecture,
creationDate: args.CreationDate,
dbType: args.Type,
description: args.Description,
ephemeral: args.Ephemeral,
expiryDate: args.ExpiryDate,
id: args.ID,
lastUsedDate: args.LastUsedDate,
localConfig: args.Config,
localDevices: args.Devices,
logger: logger.AddContext(logger.Ctx{"instanceType": args.Type, "instance": args.Name, "project": args.Project}),
name: args.Name,
node: args.Node,
profiles: args.Profiles,
project: p,
isSnapshot: args.Snapshot,
stateful: args.Stateful,
},
}
// Get the architecture name.
archName, err := osarch.ArchitectureName(d.architecture)
if err == nil {
d.architectureName = archName
}
// Cleanup the zero values.
if d.expiryDate.IsZero() {
d.expiryDate = time.Time{}
}
if d.creationDate.IsZero() {
d.creationDate = time.Time{}
}
if d.lastUsedDate.IsZero() {
d.lastUsedDate = time.Time{}
}
// This is passed during expanded config validation.
if expandedDevices != nil {
d.expandedDevices = expandedDevices
}
return d
}
// qemuCreate creates a new storage volume record and returns an initialized Instance.
// Returns a revert fail function that can be used to undo this function if a subsequent step fails.
func qemuCreate(s *state.State, args db.InstanceArgs, p api.Project, partialDeviceValidation bool, op *operations.Operation) (instance.Instance, revert.Hook, error) {
reverter := revert.New()
defer reverter.Fail()
// Create the instance struct.
d := &qemu{
common: common{
state: s,
op: op,
architecture: args.Architecture,
creationDate: args.CreationDate,
dbType: args.Type,
description: args.Description,
ephemeral: args.Ephemeral,
expiryDate: args.ExpiryDate,
id: args.ID,
lastUsedDate: args.LastUsedDate,
localConfig: args.Config,
localDevices: args.Devices,
logger: logger.AddContext(logger.Ctx{"instanceType": args.Type, "instance": args.Name, "project": args.Project}),
name: args.Name,
node: args.Node,
profiles: args.Profiles,
project: p,
isSnapshot: args.Snapshot,
stateful: args.Stateful,
},
}
// Get the architecture name.
archName, err := osarch.ArchitectureName(d.architecture)
if err == nil {
d.architectureName = archName
}
// Cleanup the zero values.
if d.expiryDate.IsZero() {
d.expiryDate = time.Time{}
}
if d.creationDate.IsZero() {
d.creationDate = time.Time{}
}
if d.lastUsedDate.IsZero() {
d.lastUsedDate = time.Time{}
}
if args.Snapshot {
d.logger.Info("Creating instance snapshot", logger.Ctx{"ephemeral": d.ephemeral})
} else {
d.logger.Info("Creating instance", logger.Ctx{"ephemeral": d.ephemeral})
}
// Load the config.
err = d.init()
if err != nil {
return nil, nil, fmt.Errorf("Failed to expand config: %w", err)
}
// When not a snapshot, perform full validation.
if !args.Snapshot {
// Validate expanded config (allows mixed instance types for profiles).
err = instance.ValidConfig(s.OS, d.expandedConfig, true, instancetype.Any)
if err != nil {
return nil, nil, fmt.Errorf("Invalid config: %w", err)
}
err = instance.ValidDevices(s, d.project, d.Type(), d.localDevices, d.expandedDevices)
if err != nil {
return nil, nil, fmt.Errorf("Invalid devices: %w", err)
}
}
// Retrieve the instance's storage pool.
_, rootDiskDevice, err := d.getRootDiskDevice()
if err != nil {
return nil, nil, fmt.Errorf("Failed getting root disk: %w", err)
}
if rootDiskDevice["pool"] == "" {
return nil, nil, errors.New("The instance's root device is missing the pool property")
}
// Initialize the storage pool.
d.storagePool, err = storagePools.LoadByName(d.state, rootDiskDevice["pool"])
if err != nil {
return nil, nil, fmt.Errorf("Failed loading storage pool: %w", err)
}
volType, err := storagePools.InstanceTypeToVolumeType(d.Type())
if err != nil {
return nil, nil, err
}
storagePoolSupported := slices.Contains(d.storagePool.Driver().Info().VolumeTypes, volType)
if !storagePoolSupported {
return nil, nil, errors.New("Storage pool does not support instance type")
}
if !d.IsSnapshot() {
// Add devices to instance.
cleanup, err := d.devicesAdd(d, false, partialDeviceValidation)
if err != nil {
return nil, nil, err
}
reverter.Add(cleanup)
}
if d.isSnapshot {
d.logger.Info("Created instance snapshot", logger.Ctx{"ephemeral": d.ephemeral})
} else {
d.logger.Info("Created instance", logger.Ctx{"ephemeral": d.ephemeral})
}
if d.isSnapshot {
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotCreated.Event(d, nil))
} else {
err = d.state.Authorizer.AddInstance(d.state.ShutdownCtx, d.project.Name, d.Name())
if err != nil {
logger.Error("Failed to add instance to authorizer", logger.Ctx{"name": d.Name(), "project": d.project.Name, "error": err})
}
reverter.Add(func() { _ = d.state.Authorizer.DeleteInstance(d.state.ShutdownCtx, d.project.Name, d.Name()) })
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceCreated.Event(d, map[string]any{
"type": api.InstanceTypeVM,
"storage-pool": d.storagePool.Name(),
"location": d.Location(),
}))
}
cleanup := reverter.Clone().Fail
reverter.Success()
return d, cleanup, err
}
// qemu is the QEMU virtual machine driver.
type qemu struct {
common
// Cached handles.
// Do not use these variables directly, instead use their associated get functions so they
// will be initialized on demand.
architectureName string
// Stateful migration streams.
migrationReceiveStateful map[string]io.ReadWriteCloser
// Indicate whether the root disk will be live-migrated.
migrationRootDisk bool
disksToMigrate []localMigration.DependentVolumeArgs
// Indicates whether this is an inner-cluster or cross-cluster move.
migrationClusterMove bool
// Keep a reference to the console socket when switching backends, so we can properly cleanup when switching back to a ring buffer.
consoleSocket *net.UnixListener
consoleSocketFile *os.File
// Keep a record of QEMU configuration.
cmdArgs []string
conf []cfg.Section
}
// qmpConnect connects to the QMP monitor.
func (d *qemu) qmpConnect() (*qmp.Monitor, error) {
return qmp.Connect(d.monitorPath(), qemuSerialChardevName, d.getMonitorEventHandler(), d.QMPLogFilePath(), qemuDetachDisk(d.state, d.id))
}
// getAgentClient returns the current agent client handle.
// Callers should check that the instance is running (and therefore mounted) before calling this function,
// otherwise the qmp.Connect call will fail to use the monitor socket file.
func (d *qemu) getAgentClient() (*http.Client, error) {
// Check that the VM is in a state where the agent may be reachable.
status := d.statusCode()
if !d.isRunningStatusCode(status) || status == api.Frozen {
return nil, errQemuAgentOffline
}
// Only Linux and Windows support VirtIO vsock.
if slices.Contains([]osinfo.OSType{osinfo.FreeBSD, osinfo.MacOS}, d.GuestOS()) {
// Get known network details.
networks, err := d.getNetworkState()
if err != nil {
return nil, errQemuAgentOffline
}
// The connection uses mutual authentication, so use the server's key & cert for client.
agentCert, _, clientCert, clientKey, err := d.generateAgentCert()
if err != nil {
return nil, err
}
// Get the TLS configuration.
tlsConfig, err := localtls.GetTLSConfigMem(clientCert, clientKey, "", agentCert, false)
if err != nil {
return nil, err
}
// Setup an HTTPS client.
client := &http.Client{}
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// Replicate the headers.
req.Header = via[len(via)-1].Header
return nil
}
for _, netInterface := range networks {
for _, address := range netInterface.Addresses {
if address.Scope != "global" {
continue
}
networkAddress := net.JoinHostPort(address.Address, strconv.Itoa(ports.HTTPSDefaultPort))
client.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
DialContext: func(_ context.Context, network, addr string) (net.Conn, error) {
return net.DialTimeout("tcp", networkAddress, 100*time.Millisecond)
},
DisableKeepAlives: true,
ExpectContinueTimeout: time.Second * 3,
ResponseHeaderTimeout: time.Second * 3600,
TLSHandshakeTimeout: time.Second * 3,
}
_, err := client.Get("https://agent/")
if err == nil {
return client, nil
}
}
}
return nil, errQemuAgentOffline
}
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return nil, err
}
if !monitor.AgenStarted() || monitor.GetInstanceState() != nil {
return nil, errQemuAgentOffline
}
// The connection uses mutual authentication, so use the server's key & cert for client.
agentCert, _, clientCert, clientKey, err := d.generateAgentCert()
if err != nil {
return nil, err
}
// Existing vsock ID from volatile.
vsockID, err := d.getVsockID()
if err != nil {
return nil, err
}
agent, err := localvsock.HTTPClient(vsockID, ports.HTTPSDefaultPort, clientCert, clientKey, agentCert)
if err != nil {
return nil, err
}
return agent, nil
}
func (d *qemu) getMonitorEventHandler() func(event string, data map[string]any) {
// Create local variables from instance properties we need so as not to keep references to instance around
// after we have returned the callback function.
instProject := d.Project()
instanceName := d.Name()
s := d.state
return func(event string, data map[string]any) {
if !slices.Contains([]string{qmp.EventVMShutdown, qmp.EventVMReset, qmp.EventAgentStarted, qmp.EventAgentStopped, qmp.EventRTCChange}, event) {
return // Don't bother loading the instance from DB if we aren't going to handle the event.
}
var err error
var d *qemu // Redefine d as local variable inside callback to avoid keeping references around.
inst := instanceRefGet(instProject.Name, instanceName)
if inst == nil {
inst, err = instance.LoadByProjectAndName(s, instProject.Name, instanceName)
if err != nil {
l := logger.AddContext(logger.Ctx{"project": instProject.Name, "instance": instanceName})
// If DB not available, try loading from backup file.
l.Warn("Failed loading instance from database to handle monitor event, trying backup file", logger.Ctx{"err": err})
instancePath := filepath.Join(internalUtil.VarPath("virtual-machines"), project.Instance(instProject.Name, instanceName))
inst, err = instance.LoadFromBackup(s, instProject.Name, instancePath, false)
if err != nil {
l.Error("Failed loading instance to handle monitor event", logger.Ctx{"err": err})
return
}
}
}
d, ok := inst.(*qemu)
if !ok {
return
}
switch event {
case qmp.EventAgentStarted:
d.logger.Debug("Instance agent started")
err := d.advertiseVsockAddress()
if err != nil {
d.logger.Warn("Failed to advertise vsock address to instance agent", logger.Ctx{"err": err})
return
}
err = d.VolatileSet(map[string]string{"volatile.last_state.agent": instance.AgentStateStarted})
if err != nil {
d.logger.Error("Failed recording last agent state", logger.Ctx{"err": err})
}
s.Events.SendLifecycle(instProject.Name, lifecycle.InstanceAgentStarted.Event(d, nil))
case qmp.EventAgentStopped:
d.logger.Debug("Instance agent stopped")
err = d.VolatileSet(map[string]string{"volatile.last_state.agent": instance.AgentStateStopped})
if err != nil {
d.logger.Error("Failed recording last agent state", logger.Ctx{"err": err})
}
s.Events.SendLifecycle(instProject.Name, lifecycle.InstanceAgentStopped.Event(d, nil))
case qmp.EventVMReset:
monitor, err := d.qmpConnect()
if err == nil {
if monitor.HandleReset() {
// This RESET corresponds to a deliberate system_reset we triggered
// (e.g. the boot-config rebuild during startup), so let QEMU handle
// it internally rather than tearing the VM down.
break
}
if !d.needsFullRestart() {
// If a quick restart is possible, let QEMU handle it.
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceRestarted.Event(d, nil))
break
}
}
fallthrough
case qmp.EventVMShutdown:
var reason string
target := "stop"
entry, ok := data["reason"]
if ok {
entryStr, ok := entry.(string)
if ok {
reason = entryStr
}
}
if reason == "guest-reset" {
target = "reboot"
}
if reason == qmp.EventVMShutdownReasonDisconnect {
d.logger.Warn("Instance stopped", logger.Ctx{"target": target, "reason": data["reason"]})
} else {
d.logger.Debug("Instance stopped", logger.Ctx{"target": target, "reason": data["reason"]})
}
err = d.onStop(target, reason)
if err != nil {
d.logger.Error("Failed to cleanly stop instance", logger.Ctx{"err": err})
return
}
case qmp.EventRTCChange:
val, ok := data["offset"].(float64)
if !ok {
d.logger.Debug("No offset in data", logger.Ctx{"data": data})
return
}
err = d.onRTCChange(int(val))
if err != nil {
d.logger.Error("Failed to apply rtc change", logger.Ctx{"offset": val, "err": err})
}
}
}
}
// mount the instance's config volume if needed.
func (d *qemu) mount() (*storagePools.MountInfo, error) {
var pool storagePools.Pool
pool, err := d.getStoragePool()
if err != nil {
return nil, err
}
if d.IsSnapshot() {
mountInfo, err := pool.MountInstanceSnapshot(d, nil)
if err != nil {
return nil, err
}
return mountInfo, nil
}
mountInfo, err := pool.MountInstance(d, nil)
if err != nil {
return nil, err
}
return mountInfo, nil
}
// unmount the instance's config volume if needed.
func (d *qemu) unmount() error {
pool, err := d.getStoragePool()
if err != nil {
return err
}
err = pool.UnmountInstance(d, nil)
if err != nil {
return err
}
return nil
}
// generateAgentCert creates the necessary server key and certificate if needed.
func (d *qemu) generateAgentCert() (string, string, string, string, error) {
agentCertFile := filepath.Join(d.Path(), "agent.crt")
agentKeyFile := filepath.Join(d.Path(), "agent.key")
clientCertFile := filepath.Join(d.Path(), "agent-client.crt")
clientKeyFile := filepath.Join(d.Path(), "agent-client.key")
// Create server certificate.
err := localtls.FindOrGenCert(agentCertFile, agentKeyFile, false, false)
if err != nil {
return "", "", "", "", err
}
// Create client certificate.
err = localtls.FindOrGenCert(clientCertFile, clientKeyFile, true, false)
if err != nil {
return "", "", "", "", err
}
// Read all the files
agentCert, err := os.ReadFile(agentCertFile)
if err != nil {
return "", "", "", "", err
}
agentKey, err := os.ReadFile(agentKeyFile)
if err != nil {
return "", "", "", "", err
}
clientCert, err := os.ReadFile(clientCertFile)
if err != nil {
return "", "", "", "", err
}
clientKey, err := os.ReadFile(clientKeyFile)
if err != nil {
return "", "", "", "", err
}
return string(agentCert), string(agentKey), string(clientCert), string(clientKey), nil
}
// Freeze freezes the instance.
func (d *qemu) Freeze() error {
// Connect to the monitor.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
// Send the stop command.
err = monitor.Pause()
if err != nil {
return err
}
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstancePaused.Event(d, nil))
return nil
}
// configDriveMountPath returns the path for the config drive bind mount.
func (d *qemu) configDriveMountPath() string {
return filepath.Join(d.DevicesPath(), "config.mount")
}
// configDriveMountPathClear attempts to unmount the config drive bind mount and remove the directory.
func (d *qemu) configDriveMountPathClear() error {
return device.DiskMountClear(d.configDriveMountPath())
}
// pidWait waits for the QEMU process to exit. Does this in a way that doesn't require the process to be a
// parent of the QEMU process (in order to allow for the daemon to be restarted after the VM was started).
// Returns true if process stopped, false if timeout was exceeded.
func (d *qemu) pidWait(timeout time.Duration) bool {
waitUntil := time.Now().Add(timeout)
for {
pid, _ := d.pid()
if pid <= 0 {
break
}
if time.Now().After(waitUntil) {
return false
}
time.Sleep(time.Millisecond * time.Duration(250))
}
return true
}
// onStop is run when the instance stops.
func (d *qemu) onStop(target string, reason string) error {
d.logger.Debug("onStop hook started", logger.Ctx{"target": target, "reason": reason})
defer d.logger.Debug("onStop hook finished", logger.Ctx{"target": target, "reason": reason})
// Create/pick up operation.
op, err := d.onStopOperationSetup(target)
if err != nil {
return err
}
// Unlock on return
defer op.Done(nil)
// Set operation if missing.
if d.op == nil {
d.op = op.GetOperation()
}
// If QEMU is still running, stop it (handles reboot).
monitor, err := qmp.Connect(d.monitorPath(), qemuSerialChardevName, nil, d.QMPLogFilePath(), qemuDetachDisk(d.state, d.id))
if err == nil {
_ = monitor.Quit()
}
// Wait for QEMU process to end (to avoiding racing start when restarting).
// Wait up to 5 minutes to allow for flushing any pending data to disk.
d.logger.Debug("Waiting for VM process to finish")
waitTimeout := time.Minute * 5
if d.pidWait(waitTimeout) {
d.logger.Debug("VM process finished")
} else {
// Log a warning, but continue clean up as best we can.
d.logger.Error("VM process failed to stop", logger.Ctx{"timeout": waitTimeout})
}
// Fully cleanup the existing QEMU monitor.
if monitor != nil {
monitor.Disconnect()
}
// Record power state.
err = d.VolatileSet(map[string]string{
"volatile.last_state.power": instance.PowerStateStopped,
"volatile.last_state.ready": "false",
})
if err != nil {
// Don't return an error here as we still want to cleanup the instance even if DB not available.
d.logger.Error("Failed recording last power state", logger.Ctx{"err": err})
}
// Cleanup.
d.cleanupDevices() // Must be called before unmount.
_ = os.Remove(d.pidFilePath())
_ = os.Remove(d.monitorPath())
_ = os.Remove(d.spicePath())
// Stop the storage for the instance.
err = d.unmount()
if err != nil && !errors.Is(err, storageDrivers.ErrInUse) {
err = fmt.Errorf("Failed unmounting instance: %w", err)
op.Done(err)
return err
}
// Unload the apparmor profile
err = apparmor.InstanceUnload(d.state.OS, d)
if err != nil {
op.Done(err)
return err
}
// Determine if instance should be auto-restarted.
cleanShutdown := reason == qmp.EventVMShutdownReasonGuestShutdown || reason == qmp.EventVMShutdownReasonQuit
var autoRestart bool
if target != "reboot" && !cleanShutdown && d.shouldAutoRestart() {
autoRestart = true
// Mark current shutdown as complete.
op.Done(nil)
// Create a new restart operation.
op, err = operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionRestart, nil, true, false)
if err == nil {
defer op.Done(nil)
} else {
d.logger.Error("Failed to setup new restart operation", logger.Ctx{"err": err})
}
}
// Log and emit lifecycle if not user triggered.
if target != "reboot" && !autoRestart && op.Action() != operationlock.ActionMigrate && op.Action() != operationlock.ActionRestart {
if op.GetInstanceInitiated() {
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceShutdown.Event(d, nil))
} else {
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceStopped.Event(d, nil))
}
// Only trigger if agent state not stopped and update accordingly
if d.LocalConfig()["volatile.last_state.agent"] == instance.AgentStateStarted {
err = d.VolatileSet(map[string]string{"volatile.last_state.agent": instance.AgentStateStopped})
if err != nil {
d.logger.Error("Failed recording last ready state", logger.Ctx{"err": err})
}
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceAgentStopped.Event(d, nil))
}
}
// Reboot the instance.
if target == "reboot" || autoRestart {
err = d.Start(false)
if err != nil {
op.Done(err)
return err
}
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceRestarted.Event(d, nil))
} else if d.ephemeral {
// Destroy ephemeral virtual machines.
err = d.delete(true, true)
if err != nil {
op.Done(err)
return err
}
}
return nil
}
// Shutdown shuts the instance down.
func (d *qemu) Shutdown(timeout time.Duration) error {
d.logger.Debug("Shutdown started", logger.Ctx{"timeout": timeout})
defer d.logger.Debug("Shutdown finished", logger.Ctx{"timeout": timeout})
// Must be run prior to creating the operation lock.
statusCode := d.statusCode()
if !d.isRunningStatusCode(statusCode) {
if statusCode == api.Error {
return fmt.Errorf("The instance cannot be cleanly shutdown as in %s status", statusCode)
}
return ErrInstanceIsStopped
}
// Save the console log from ring buffer before the instance is shutdown. Must be run prior to creating the operation lock.
_, err := d.ConsoleLog()
if err != nil {
return err
}
// Setup a new operation.
// Allow inheriting of ongoing restart operation (we are called from restartCommon).
// Allow reuse when creating a new stop operation. This allows the Stop() function to inherit operation.
// Allow reuse of a reusable ongoing stop operation as Shutdown() may be called earlier, which allows reuse
// of its operations. This allow for multiple Shutdown() attempts.
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionStop, []operationlock.Action{operationlock.ActionRestart}, true, true)
if err != nil {
if errors.Is(err, operationlock.ErrNonReusuableSucceeded) {
// An existing matching operation has now succeeded, return.
return nil
}
return err
}
// If frozen, resume so the signal can be handled.
if d.IsFrozen() {
err := d.Unfreeze()
if err != nil {
return err
}
}
// Connect to the monitor.
monitor, err := d.qmpConnect()
if err != nil {
op.Done(err)
return err
}
// Indicate to the onStop hook that if the VM stops it was due to a clean shutdown because the VM responded
// to the powerdown request.
op.SetInstanceInitiated(true)
// Send the system_powerdown command.
err = monitor.Powerdown()
if err != nil {
if errors.Is(err, qmp.ErrMonitorDisconnect) {
op.Done(nil)
return nil
}
op.Done(err)
return err
}
// Wait 500ms for the first event to be received by the guest.
time.Sleep(500 * time.Millisecond)
// Attempt to send a second system_powerdown command (required to get Windows to shutdown).
_ = monitor.Powerdown()
d.logger.Debug("Shutdown request sent to instance")
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Wait for operation lock to be Done or context to timeout. The operation lock is normally completed by
// onStop which picks up the same lock and then marks it as Done after the instance stops and the devices
// have been cleaned up. However if the operation has failed for another reason we collect the error here.
err = op.Wait(ctx)
status := d.statusCode()
if status != api.Stopped {
errPrefix := fmt.Errorf("Failed shutting down instance, status is %q", status)
if err != nil {
return fmt.Errorf("%s: %w", errPrefix.Error(), err)
}
return errPrefix
}
// Now handle errors from shutdown sequence and return to caller if wasn't completed cleanly.
if err != nil {
return err
}
return nil
}
// Restart restart the instance.
func (d *qemu) Restart(timeout time.Duration) error {
return d.restartCommon(d, timeout)
}
// Rebuild rebuilds the instance using the supplied image fingerprint as source.
func (d *qemu) Rebuild(img *api.Image, op *operations.Operation) error {
return d.rebuildCommon(d, img, op)
}
// killQemuProcess kills specified process. Optimistically attempts to wait for the process to fully exit, but does
// not return an error if the Wait call fails. This is because this function is used in scenarios where the daemon has
// been restarted after the VM has been started and is no longer the parent of the QEMU process.
// The caller should use another method to ensure that the QEMU process has fully exited instead.
// Returns an error if the Kill signal couldn't be sent to the process (for any other reason apart from the process
// not existing).
func (d *qemu) killQemuProcess(pid int) error {
proc, err := os.FindProcess(pid)
if err != nil {
if errors.Is(err, os.ErrProcessDone) {
return nil
}
return err
}
err = proc.Kill()
if err != nil {
if errors.Is(err, os.ErrProcessDone) {
return nil
}
return err
}
// Wait for process to exit, but don't return an error if this fails as it may be called when the daemon isn't
// the parent of the process, and we have still sent the kill signal as per the function's description.
_, err = proc.Wait()
if err != nil {
if strings.Contains(err.Error(), "no child processes") {
return nil
}
d.logger.Warn("Failed to collect VM process exit status", logger.Ctx{"pid": pid, "err": err})
}
return nil
}
// restoreState restores the VM state from a file handle.
func (d *qemu) restoreStateHandle(ctx context.Context, monitor *qmp.Monitor, f *os.File) error {
err := monitor.SendFile("migration", f)
if err != nil {
return err
}
err = monitor.MigrateIncoming(ctx, "migration")
if err != nil {
if errors.Is(err, qmp.ErrMonitorDisconnect) && util.PathExists(d.LogFilePath()) {
qemuError, err := os.ReadFile(d.LogFilePath())
if err != nil {
return err
}
return fmt.Errorf("QEMU crashed on VM restore: %s", string(qemuError))
}
return err
}
return nil
}
// receiveMigrationSnapshot handles an incoming disk snapshot during migration.
func (d *qemu) receiveMigrationSnapshot(monitor *qmp.Monitor, blockExport string, filesystemConn io.ReadWriteCloser) error {
nbdConn, err := monitor.NBDServerStart("", 1)
if err != nil {
return fmt.Errorf("Failed starting NBD server: %w", err)
}
d.logger.Debug("Migration NBD server started")
defer func() {
_ = nbdConn.Close()
_ = monitor.NBDServerStop()
}()
err = monitor.NBDBlockExportAdd(blockExport, blockExport, true, "", nil)
if err != nil {
return fmt.Errorf("Failed adding root disk to NBD server: %w", err)
}
d.logger.Debug("Migration storage NBD export starting")
go func() { _, _ = util.SafeCopy(filesystemConn, nbdConn) }()
_, _ = util.SafeCopy(nbdConn, filesystemConn)
filesystemConn.Close()
d.logger.Debug("Migration storage NBD export finished")
return nil
}
// restoreState restores VM state from state file or from migration source if d.migrationReceiveStateful set.
func (d *qemu) restoreState(monitor *qmp.Monitor) error {
if d.migrationReceiveStateful != nil {
stateConn := d.migrationReceiveStateful[api.SecretNameState]
if stateConn == nil {
return errors.New("Migration state connection is not initialized")
}
// Perform non-shared storage transfer if requested.
filesystemConn := d.migrationReceiveStateful[api.SecretNameFilesystem]
if filesystemConn != nil {
go func() {
if d.migrationRootDisk {
rootDiskName, _, err := internalInstance.GetRootDiskDevice(d.expandedDevices.CloneNative())
if err != nil {
d.logger.Error("Failed getting instance root disk", logger.Ctx{"err": err})
return
}
escapedDeviceName := linux.PathNameEncode(rootDiskName)
rootDiskName = d.blockNodeName(escapedDeviceName)
err = d.receiveMigrationSnapshot(monitor, rootDiskName, filesystemConn)
if err != nil {
d.logger.Error("Failed receiving migration snapshot", logger.Ctx{"err": err})
return
}
}
pool, err := d.getStoragePool()
if err != nil {
d.logger.Error("Failed fetching instance storage pool", logger.Ctx{"err": err})
return
}
config, err := pool.GenerateInstanceBackupConfig(d, false, true, nil)
if err != nil {
d.logger.Error("Failed generating instance backup config", logger.Ctx{"err": err})
return
}
devicesMap := storagePools.DevicesMapFromBackupConfig(config)
for _, vol := range d.disksToMigrate {
d.logger.Debug("Receiving dependent volume", logger.Ctx{"name": vol.Name, "pool": vol.Pool})
deviceName := storagePools.DeviceByPoolAndVolume(devicesMap, vol.Pool, vol.Name)
if deviceName == "" {
d.logger.Error("Failed to find requested device", logger.Ctx{"pool": vol.Pool, "volName": vol.Name})
return
}
diskName := d.blockNodeName(linux.PathNameEncode(deviceName))
err = d.receiveMigrationSnapshot(monitor, diskName, filesystemConn)
if err != nil {
d.logger.Error("Failed receiving migration snapshot", logger.Ctx{"err": err})
}
}
}()
}
// Receive checkpoint from QEMU process on source.
d.logger.Debug("Stateful migration checkpoint receive starting")
pipeRead, pipeWrite, err := os.Pipe()
if err != nil {
return err
}
go func() {
_, _ = util.SafeCopy(pipeWrite, stateConn)
_ = pipeRead.Close()
_ = pipeWrite.Close()
}()
err = d.restoreStateHandle(context.Background(), monitor, pipeRead)
if err != nil {
return fmt.Errorf("Failed restoring checkpoint from source: %w", err)
}
d.logger.Debug("Stateful migration checkpoint receive finished")
} else {
statePath := d.StatePath()
d.logger.Debug("Stateful checkpoint restore starting", logger.Ctx{"source": statePath})
defer d.logger.Debug("Stateful checkpoint restore finished", logger.Ctx{"source": statePath})
stateFile, err := os.Open(statePath)
if err != nil {
return fmt.Errorf("Failed opening state file %q: %w", statePath, err)
}
defer logger.WarnOnError(stateFile.Close, "Failed to close state file")
uncompressedState, err := gzip.NewReader(stateFile)
if err != nil {
return fmt.Errorf("Failed opening state gzip reader: %w", err)
}
defer logger.WarnOnError(uncompressedState.Close, "Failed to close state reader")
pipeRead, pipeWrite, err := os.Pipe()
if err != nil {
return err
}
go func() {
_, err := util.SafeCopy(pipeWrite, uncompressedState)
if err != nil {
d.logger.Warn("Failed reading from state file", logger.Ctx{"path": statePath, "err": err})
}
_ = pipeRead.Close()
_ = pipeWrite.Close()
}()
err = d.restoreStateHandle(context.Background(), monitor, pipeRead)
if err != nil {
return fmt.Errorf("Failed restoring state from %q: %w", stateFile.Name(), err)
}
}
return nil
}
// saveStateHandle dumps the current VM state to a file handle.
// Once started, the VM is in a paused state and it's up to the caller to wait for the transfer to complete and
// resume or kill the VM guest.
func (d *qemu) saveStateHandle(monitor *qmp.Monitor, f *os.File) error {
// Send the target file to qemu.
err := monitor.SendFile("migration", f)
if err != nil {
return err
}
// Issue the migration command.
err = monitor.Migrate("migration")
if err != nil {
return err
}
return nil
}
// saveState dumps the current VM state to the state file.
// Once dumped, the VM is in a paused state and it's up to the caller to resume or kill it.
func (d *qemu) saveState(monitor *qmp.Monitor) error {
statePath := d.StatePath()
d.logger.Debug("Stateful checkpoint starting", logger.Ctx{"target": statePath})
defer d.logger.Debug("Stateful checkpoint finished", logger.Ctx{"target": statePath})
// Save the checkpoint to state file.
_ = os.Remove(statePath)
// Prepare the state file.
stateFile, err := os.Create(statePath)
if err != nil {
return err
}
defer logger.WarnOnError(stateFile.Close, "Failed to close state file")
compressedState, err := gzip.NewWriterLevel(stateFile, gzip.BestSpeed)
if err != nil {
return err
}
defer logger.WarnOnError(compressedState.Close, "Failed to close state writer")
pipeRead, pipeWrite, err := os.Pipe()
if err != nil {
return err
}
defer func() {
_ = pipeRead.Close()
_ = pipeWrite.Close()
}()
go func() { _, _ = util.SafeCopy(compressedState, pipeRead) }()
err = d.saveStateHandle(monitor, pipeWrite)
if err != nil {
return fmt.Errorf("Failed initializing state save to %q: %w", stateFile.Name(), err)
}
err = monitor.MigrateWait(context.Background(), "completed")
if err != nil {
return fmt.Errorf("Failed saving state to %q: %w", stateFile.Name(), err)
}
return nil
}
// validateStartup checks any constraints that would prevent start up from succeeding under normal circumstances.
func (d *qemu) validateStartup(stateful bool, statusCode api.StatusCode) error {
err := d.common.validateStartup(stateful, statusCode)
if err != nil {
return err
}
// Cannot perform stateful start unless config is appropriately set.
// NOTE: We can't use CanLiveMigrate during instance startup as the boot state hasn't yet been recorded.
if stateful && util.IsFalseOrEmpty(d.expandedConfig["migration.stateful"]) {
return errors.New("Stateful start requires migration.stateful to be set to true")
}
// gendoc:generate(entity=image, group=requirements, key=requirements.secureboot)
//
// ---
// type: bool
// shortdesc: If set to `false`, indicates that the image cannot boot under secure boot.
//
// Ensure secureboot is turned off for images that are not secureboot enabled.
if util.IsFalse(d.localConfig["image.requirements.secureboot"]) && util.IsTrueOrEmpty(d.expandedConfig["security.secureboot"]) {
return errors.New("The image used by this instance is incompatible with secureboot. Please set security.secureboot=false on the instance")
}
// Ensure secureboot is turned off when CSM is on.
if util.IsTrue(d.expandedConfig["security.csm"]) && util.IsTrueOrEmpty(d.expandedConfig["security.secureboot"]) {
return errors.New("Secure boot can't be enabled while CSM is turned on. Please set security.secureboot=false on the instance")
}
// gendoc:generate(entity=image, group=requirements, key=requirements.cdrom_agent)
//
// ---
// type: bool
// shortdesc: If set to `true`, indicates that the VM requires an `agent:config` disk be added.
//
// Ensure an agent drive is present if the image requires it.
if util.IsTrue(d.localConfig["image.requirements.cdrom_agent"]) {
found := false
for _, dev := range d.expandedDevices {
if dev["type"] == "disk" && dev["source"] == "agent:config" {
found = true
break
}
}
if !found {
return errors.New("This virtual machine image requires an agent:config disk be added")
}
}
// gendoc:generate(entity=image, group=requirements, key=requirements.cdrom_cloud_init)
//
// ---
// type: bool
// shortdesc: If set to `true`, indicates that the VM requires a `cloud-init:config` disk be present every time `cloud-init` should be run.
//
// Ensure a `cloud-init` drive is present if the image and the VM state require it.
if util.IsTrue(d.localConfig["image.requirements.cdrom_cloud_init"]) && d.localConfig["volatile.apply_template"] != "" {
found := false
for _, dev := range d.expandedDevices {
if dev["type"] == "disk" && dev["source"] == "cloud-init:config" {
found = true
break
}
}
if !found {
return errors.New("This virtual machine image requires a cloud-init:config disk be added")
}
}
return nil
}
func (d *qemu) checkStateStorage() error {
// For some operations, the "size.state" of the instance root disk device must be larger than the instance memory.
// Otherwise, there will not be enough disk space to write the instance state to disk during any subsequent stops.
// (Only check when migration.stateful is true, otherwise the memory won't be dumped when this instance stops).
_, rootDiskDevice, err := d.getRootDiskDevice()
if err != nil {
return err
}
// Don't access d.storagePool directly since it isn't populated at this stage.
pool, err := d.getStoragePool()
if err != nil {
return err
}
stateDiskSizeStr := pool.Driver().Info().DefaultVMBlockFilesystemSize
if rootDiskDevice["size.state"] != "" {
stateDiskSizeStr = rootDiskDevice["size.state"]
}
stateDiskSize, err := units.ParseByteSizeString(stateDiskSizeStr)
if err != nil {
return err
}
memoryLimitStr := qemudefault.MemSize
if d.expandedConfig["limits.memory"] != "" {
memoryLimitStr = d.expandedConfig["limits.memory"]
}
memoryLimit, err := ParseMemoryStr(memoryLimitStr)
if err != nil {
return err
}
if stateDiskSize < memoryLimit {
return errors.New("Stateful stop and snapshots require the instance limits.memory be less than or equal to the root disk size.state property")
}
return nil
}
// Start starts the instance.
func (d *qemu) Start(stateful bool) error {
return d.start(stateful, nil)
}
// runStartupScriptlet runs startup scriptlets at config, early, pre-start and post-start stages.
func (d *qemu) runStartupScriptlet(monitor *qmp.Monitor, stage string) error {
_, ok := d.expandedConfig["raw.qemu.scriptlet"]
if ok {
// Render cannot return errors here.
render, _, _ := d.Render()
instanceData, ok := render.(*api.Instance)
if !ok {
return errors.New("Unexpected instance type")
}
err := scriptlet.QEMURun(logger.Log, instanceData, &d.cmdArgs, &d.conf, monitor, stage)
if err != nil {
err = fmt.Errorf("Failed running QEMU scriptlet at %s stage: %w", stage, err)
return err
}
}
return nil
}
// startupHook executes QMP commands and runs startup scriptlets at early, pre-start and post-start
// stages.
func (d *qemu) startupHook(monitor *qmp.Monitor, stage string) error {
commands, ok := d.expandedConfig["raw.qemu.qmp."+stage]
if ok {
var commandList []map[string]any
err := json.Unmarshal([]byte(commands), &commandList)
if err != nil {
err = fmt.Errorf("Failed to parse QMP commands at %s stage (expected JSON list of objects): %w", stage, err)
return err
}
for _, command := range commandList {
id := monitor.IncreaseID()
command["id"] = id
var jsonCommand []byte
jsonCommand, err = json.Marshal(command)
if err != nil {
err = fmt.Errorf("Failed to marshal command at %s stage: %w", stage, err)
return err
}
err = monitor.RunJSON(jsonCommand, nil, true, id)
if err != nil {
err = fmt.Errorf("Failed to run QMP command %s at %s stage: %w", jsonCommand, stage, err)
return err
}
}
}
return d.runStartupScriptlet(monitor, stage)
}
// start starts the instance and can use an existing InstanceOperation lock.
func (d *qemu) start(stateful bool, op *operationlock.InstanceOperation) error {
d.logger.Debug("Start started", logger.Ctx{"stateful": stateful})
defer d.logger.Debug("Start finished", logger.Ctx{"stateful": stateful})
// Check that we are startable before creating an operation lock.
// Must happen before creating operation Start lock to avoid the status check returning Stopped due to the
// existence of a Start operation lock.
err := d.validateStartup(stateful, d.statusCode())
if err != nil {
return err
}
// Setup a new operation if needed.
if op == nil {
op, err = operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionStart, []operationlock.Action{operationlock.ActionRestart, operationlock.ActionRestore}, false, false)
if err != nil {
if errors.Is(err, operationlock.ErrNonReusuableSucceeded) {
// An existing matching operation has now succeeded, return.
return nil
}
return fmt.Errorf("Failed to create instance start operation: %w", err)
}
}
defer op.Done(err)
// Record (or load) boot state.
bs := &qemuBootState{
Version: qemuBootStateVersion,
}
if stateful {
bs, err = d.getBootState()
if err != nil {
return err
}
}
// Assign NUMA node(s) if needed.
if d.expandedConfig["limits.cpu.nodes"] == "balanced" {
err := d.balanceNUMANodes()
if err != nil {
op.Done(err)
return err
}
}
// Ensure the correct vhost_vsock kernel module is loaded before establishing the vsock.
err = linux.LoadModule("vhost_vsock")
if err != nil {
op.Done(err)
return err
}
reverter := revert.New()
defer reverter.Fail()
// Rotate the log files.
for _, logfile := range []string{d.LogFilePath(), d.ConsoleBufferLogPath(), d.QMPLogFilePath()} {
if util.PathExists(logfile) {
_ = os.Remove(logfile + ".old")
err := os.Rename(logfile, logfile+".old")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
op.Done(err)
return err
}
}
}
// Remove old pid file if needed.
if util.PathExists(d.pidFilePath()) {
err = os.Remove(d.pidFilePath())
if err != nil {
op.Done(err)
return fmt.Errorf("Failed removing old PID file %q: %w", d.pidFilePath(), err)
}
}
// Cleanup old sockets.
for _, socketPath := range []string{d.consolePath(), d.spicePath(), d.monitorPath(), d.nbdPath()} {
_ = os.Remove(socketPath)
}
// Mount the instance's config volume.
mountInfo, err := d.mount()
if err != nil {
op.Done(err)
return err
}
reverter.Add(func() { _ = d.unmount() })
// Define a set of files to open and pass their file descriptors to QEMU command.
fdFiles := make([]*os.File, 0)
// Ensure passed files are closed after start has returned (either because QEMU has started or on error).
defer func() {
for _, file := range fdFiles {
_ = file.Close()
}
}()
// New or existing vsock ID from volatile.
vsockID, vsockF, err := d.nextVsockID()
if err != nil {
return err
}
// Add allocated QEMU vhost file descriptor.
vsockFD := d.addFileDescriptor(&fdFiles, vsockF)
volatileSet := make(map[string]string)
if !stateful {
volatileSet["volatile.vm.needs_reset"] = ""
}
// Update vsock ID in volatile if needed for recovery (do this before UpdateBackupFile() call).
oldVsockID := d.localConfig["volatile.vsock_id"]
newVsockID := strconv.FormatUint(uint64(vsockID), 10)
if oldVsockID != newVsockID {
volatileSet["volatile.vsock_id"] = newVsockID
}
// Generate UUID if not present (do this before UpdateBackupFile() call).
instUUID := d.localConfig["volatile.uuid"]
if instUUID == "" {
instUUID = uuid.New().String()
volatileSet["volatile.uuid"] = instUUID
}
// For a VM instance, we must also set the VM generation ID.
vmGenUUID := d.localConfig["volatile.uuid.generation"]
if vmGenUUID == "" {
vmGenUUID = instUUID
volatileSet["volatile.uuid.generation"] = vmGenUUID
}
// Generate the config drive.
err = d.generateConfigShare(volatileSet)
if err != nil {
op.Done(err)
return err
}
// Create all needed paths.
err = os.MkdirAll(d.LogPath(), 0o700)
if err != nil {
op.Done(err)
return err
}
err = os.MkdirAll(d.RunPath(), 0o700)
if err != nil {
op.Done(err)
return err
}
err = os.MkdirAll(d.DevicesPath(), 0o711)
if err != nil {
op.Done(err)
return err
}
err = os.MkdirAll(d.ShmountsPath(), 0o711)
if err != nil {
op.Done(err)
return err
}
// Copy EDK2 settings firmware to nvram file if needed.
// Set up EDK2 NVRAM when on EFI.
if d.architectureSupportsUEFI(d.architecture) {
fi, err := os.Lstat(d.nvramPath())
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
// Generate new NVRAM if missing, or if requested by the user or if the NVRAM file is of an invalid format (needs to be a valid symlink).
if util.IsTrue(d.localConfig["volatile.apply_nvram"]) || fi == nil || fi.Mode()&os.ModeSymlink != os.ModeSymlink {
err = d.setupNvram()
if err != nil {
op.Done(err)
return err
}
}
}
// Clear volatile.apply_nvram if set.
if d.localConfig["volatile.apply_nvram"] != "" {
volatileSet["volatile.apply_nvram"] = ""
}
// Apply any volatile changes that need to be made.
err = d.VolatileSet(volatileSet)
if err != nil {
err = fmt.Errorf("Failed setting volatile keys: %w", err)
op.Done(err)
return err
}
devConfs := make([]*deviceConfig.RunConfig, 0, len(d.expandedDevices))
postStartHooks := []func() error{}
sortedDevices := d.expandedDevices.Sorted()
startDevices := make([]device.Device, 0, len(sortedDevices))
// Load devices in sorted order, this ensures that device mounts are added in path order.
// Loading all devices first means that validation of all devices occurs before starting any of them.
for _, entry := range sortedDevices {
dev, err := d.deviceLoad(d, entry.Name, entry.Config, false)
if err != nil {
if errors.Is(err, device.ErrUnsupportedDevType) {
continue // Skip unsupported device (allows for mixed instance type profiles).
}
err = fmt.Errorf("Failed start validation for device %q: %w", entry.Name, err)
op.Done(err)
return err
}
// Run pre-start of check all devices before starting any device to avoid expensive revert.
err = dev.PreStartCheck()
if err != nil {
op.Done(err)
return fmt.Errorf("Failed pre-start check for device %q: %w", dev.Name(), err)
}
startDevices = append(startDevices, dev)
}
// Start devices in order.
for i := range startDevices {
dev := startDevices[i] // Local var for revert.
// Start the device.
runConf, err := d.deviceStart(dev, false)
if err != nil {
err = fmt.Errorf("Failed to start device %q: %w", dev.Name(), err)
op.Done(err)
return err
}
reverter.Add(func() {
err := d.deviceStop(dev, false, "")
if err != nil {
d.logger.Error("Failed to cleanup device", logger.Ctx{"device": dev.Name(), "err": err})
}
})
if runConf == nil {
continue
}
if runConf.Revert != nil {
reverter.Add(runConf.Revert)
}
// Add post-start hooks
if len(runConf.PostHooks) > 0 {
postStartHooks = append(postStartHooks, runConf.PostHooks...)
}
devConfs = append(devConfs, runConf)
}
// Setup the config drive readonly bind mount. Important that this come after the root disk device start.
// in order to allow unmounts triggered by deferred resizes of the root volume.
configMntPath := d.configDriveMountPath()
err = d.configDriveMountPathClear()
if err != nil {
err = fmt.Errorf("Failed cleaning config drive mount path %q: %w", configMntPath, err)
op.Done(err)
return err
}
err = os.Mkdir(configMntPath, 0o700)
if err != nil {
err = fmt.Errorf("Failed creating device mount path %q for config drive: %w", configMntPath, err)
op.Done(err)
return err
}
reverter.Add(func() { _ = d.configDriveMountPathClear() })
// Mount the config drive device as readonly. This way it will be readonly irrespective of whether its
// exported via 9p for virtio-fs.
configSrcPath := filepath.Join(d.Path(), "config")
err = device.DiskMount(configSrcPath, configMntPath, false, "", []string{"ro"}, "none")
if err != nil {
err = fmt.Errorf("Failed mounting device mount path %q for config drive: %w", configMntPath, err)
op.Done(err)
return err
}
// Get qemu configuration and check qemu is installed.
qemuPath, qemuBus, err := d.qemuArchConfig(d.architecture)
if err != nil {
op.Done(err)
return err
}
// Snapshot if needed.
snapName, expiry, err := d.getStartupSnapNameAndExpiry(d)
if err != nil {
err = fmt.Errorf("Failed getting startup snapshot info: %w", err)
op.Done(err)
return err
}
if snapName != "" && expiry != nil {
err := d.snapshot(snapName, *expiry, false)
if err != nil {
err = fmt.Errorf("Failed taking startup snapshot: %w", err)
op.Done(err)
return err
}
}
// Setup the CPU.
if bs.CPUTopology == nil {
// Get the CPU topology.
cpuTopology, err := d.cpuTopology()
if err != nil {
return err
}
bs.CPUTopology = cpuTopology
} else if !bs.CPUTopology.Explicit {
// Re-compute the topology if the current configuration uses CPU pinning.
// The pins are host-specific so may have changed, but the topology must keep the same shape.
cpuTopology, err := d.cpuTopology()
if err != nil {
return err
}
if cpuTopology.VCPUs != nil {
if cpuTopology.Sockets != bs.CPUTopology.Sockets || cpuTopology.Cores != bs.CPUTopology.Cores || cpuTopology.Threads != bs.CPUTopology.Threads {
err = errors.New("Current CPU topology doesn't match the topology the instance was started with")
op.Done(err)
return err
}
bs.CPUTopology = cpuTopology
}
}
if bs.CPUType == "" {
cpuType, err := d.cpuType(bs)
if err != nil {
return err
}
bs.CPUType = cpuType
}
// Setup the memory.
if bs.MemoryTopology == nil {
// Get the memory topology.
memoryTopology, err := d.memoryTopology(bs)
if err != nil {
return err
}
bs.MemoryTopology = memoryTopology
}
// Generate the QEMU configuration.
monHooks, err := d.generateQemuConfig(bs, mountInfo, qemuBus, vsockFD, devConfs, &fdFiles)
if err != nil {
op.Done(err)
return err
}
confFile := filepath.Join(d.RunPath(), "qemu.conf")
// Start QEMU.
qemuArgs := []string{
"-S",
"-name", d.Name(),
"-uuid", instUUID,
"-daemonize",
"-cpu", bs.CPUType,
"-nographic",
"-serial", "chardev:console",
"-nodefaults",
"-no-user-config",
"-sandbox", "on,obsolete=deny,elevateprivileges=allow,spawn=allow,resourcecontrol=deny",
"-readconfig", confFile,
"-pidfile", d.pidFilePath(),
"-D", d.LogFilePath(),
}
// Get the feature flags.
info := DriverStatuses()[instancetype.VM].Info
_, spiceSupported := info.Features["spice"]
if spiceSupported {
spiceConfig, err := d.spiceCmdlineConfig(&fdFiles)
if err != nil {
op.Done(err)
return err
}
qemuArgs = append(qemuArgs, "-spice", spiceConfig)
}
// When a GPU is using virtio-gpu DRM native context, the guest needs a host-backed
// GL display. Switch the default headless setup to egl-headless and point it at the
// resolved render node (if any) so rendering is offloaded to the host GPU.
nativeContext, rendernode, _ := d.gpuNativeContextConfig(devConfs)
if nativeContext {
display := "egl-headless,gl=on"
if rendernode != "" {
display += fmt.Sprintf(",rendernode=%s", rendernode)
}
qemuArgs = append(qemuArgs, "-display", display)
}
// If stateful, restore now.
if stateful {
if d.stateful {
qemuArgs = append(qemuArgs, "-incoming", "defer")
} else {
// No state to restore, just start as normal.
stateful = false
}
} else if d.stateful {
// Stateless start requested but state is present, delete it.
err := os.Remove(d.StatePath())
if err != nil && !errors.Is(err, fs.ErrNotExist) {
op.Done(err)
return err
}
d.stateful = false
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
return tx.UpdateInstanceStatefulFlag(ctx, d.id, false)
})
if err != nil {
op.Done(err)
return fmt.Errorf("Error updating instance stateful flag: %w", err)
}
}
// SMBIOS only on x86_64 and aarch64.
if d.architectureSupportsUEFI(d.architecture) {
qemuArgs = append(qemuArgs, "-smbios", "type=2,manufacturer=LinuxContainers,product=Incus")
// We'll pass the values through a file to avoid needlessly long
// command line arguments and the values being visible in the process list.
smbios11 := filepath.Join(d.RunPath(), "smbios11")
err = os.RemoveAll(smbios11)
if err != nil {
op.Done(err)
return err
}
err = os.Mkdir(smbios11, 0o700)
if err != nil {
op.Done(err)
return err
}
smbios11Idx := 0
for k, v := range d.expandedConfig {
var configPrefix, smbiosPrefix string
if strings.HasPrefix(k, "smbios11.") {
configPrefix = "smbios11."
smbiosPrefix = ""
} else if strings.HasPrefix(k, "systemd.credential.") {
configPrefix = "systemd.credential."
smbiosPrefix = "io.systemd.credential:"
} else if strings.HasPrefix(k, "systemd.credential-binary.") {
configPrefix = "systemd.credential-binary."
smbiosPrefix = "io.systemd.credential.binary:"
data, err := base64.RawStdEncoding.DecodeString(strings.TrimRight(v, "="))
if err != nil {
return fmt.Errorf("Invalid base64 value for %q: %q", k, v)
}
v = base64.StdEncoding.EncodeToString(data)
} else {
continue
}
smbios11File := filepath.Join(smbios11, strconv.Itoa(smbios11Idx))
content := fmt.Sprintf("%s%s=%s", smbiosPrefix, strings.TrimPrefix(k, configPrefix), v)
err = os.WriteFile(smbios11File, []byte(content), 0o400)
if err != nil {
op.Done(err)
return err
}
qemuArgs = append(qemuArgs, "-smbios", fmt.Sprintf("type=11,path=%s", qemuEscapeCmdline(smbios11File)))
smbios11Idx++
}
}
// Attempt to drop privileges (doesn't work when restoring state).
if !stateful && d.state.OS.UnprivUser != "" {
qemuVer, _ := d.version()
qemuVer91, _ := version.NewDottedVersion("9.1.0")
// Since QEMU 9.1 the parameter `runas` has been marked as deprecated.
if qemuVer != nil && qemuVer.Compare(qemuVer91) >= 0 {
qemuArgs = append(qemuArgs, "-run-with", fmt.Sprintf("user=%s", d.state.OS.UnprivUser))
} else {
qemuArgs = append(qemuArgs, "-runas", d.state.OS.UnprivUser)
}
nvRAMPath := d.nvramPath()
if d.architectureSupportsUEFI(d.architecture) && util.PathExists(nvRAMPath) {
// Ensure UEFI nvram file is writable by the QEMU process.
// This is needed when doing stateful snapshots because the QEMU process will reopen the
// file for writing.
err = os.Chown(nvRAMPath, int(d.state.OS.UnprivUID), -1)
if err != nil {
op.Done(err)
return err
}
err = os.Chmod(nvRAMPath, 0o600)
if err != nil {
op.Done(err)
return err
}
}
// Change ownership of main instance directory.
err = os.Chown(d.Path(), int(d.state.OS.UnprivUID), -1)
if err != nil {
op.Done(err)
return fmt.Errorf("Failed to chown instance path: %w", err)
}
// Change ownership of config directory files so they are accessible to the
// unprivileged qemu process so that the 9p share can work.
//
// Security note: The 9P share will present the UID owner of these files on the host
// to the VM. In order to ensure that non-root users in the VM cannot access these
// files be sure to mount the 9P share in the VM with the "access=0" option to allow
// only root user in VM to access the mounted share.
err := filepath.Walk(filepath.Join(d.Path(), "config"),
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
err = os.Chown(path, int(d.state.OS.UnprivUID), -1)
if err != nil {
return err
}
return nil
})
if err != nil {
op.Done(err)
return err
}
}
// Handle hugepages on architectures where we don't set NUMA nodes.
if d.architecture != osarch.ARCH_64BIT_INTEL_X86 && util.IsTrue(d.expandedConfig["limits.memory.hugepages"]) {
hugetlb, err := localUtil.HugepagesPath()
if err != nil {
op.Done(err)
return err
}
qemuArgs = append(qemuArgs, "-mem-path", hugetlb, "-mem-prealloc")
}
if d.expandedConfig["raw.qemu"] != "" {
fields, err := shellquote.Split(d.expandedConfig["raw.qemu"])
if err != nil {
op.Done(err)
return err
}
qemuArgs = append(qemuArgs, fields...)
}
// Apply the RTC configuration.
// This needs to happen close to creating the full qemu cmd or the time might drift in between.
adjustment := d.getStartupRTCAdjustment()
if d.GuestOS() == osinfo.Windows || adjustment != 0 {
base := time.Now().Add(adjustment)
if d.GuestOS() == osinfo.Windows {
// set base to localtime on windows.
base = base.Local()
} else {
// set base to UTC on !windows.
base = base.UTC()
}
datetime := base.Format("2006-01-02T15:04:05")
qemuArgs = append(qemuArgs, "-rtc", fmt.Sprintf("base=%s", datetime))
}
d.cmdArgs = qemuArgs
// Precompile the QEMU scriptlet
src, ok := d.expandedConfig["raw.qemu.scriptlet"]
if ok {
instanceName := d.Name()
err := scriptletLoad.QEMUSet(src, instanceName)
if err != nil {
err = fmt.Errorf("Failed loading QEMU scriptlet: %w", err)
return err
}
}
// Config startup hook.
err = d.runStartupScriptlet(nil, "config")
if err != nil {
op.Done(err)
return err
}
// Write the config file.
err = d.writeQemuConfigFile(confFile)
if err != nil {
op.Done(err)
return err
}
// Run the qemu command via forkqemu so we can selectively increase ulimits.
forkQemuCmd := []string{
"forkqemu",
}
if !d.state.OS.RunningInUserNS {
// Required for PCI passthrough.
forkQemuCmd = append(forkQemuCmd, "limit=memlock:unlimited:unlimited")
}
// On systems with heterogeneous CPU types, confine QEMU startup to a single
// type as KVM vCPU initialization fails when scheduled across types.
startupCPUs := d.startupCPUSet(bs.CPUTopology)
if len(startupCPUs) > 0 {
cpus := make([]string, 0, len(startupCPUs))
for _, id := range startupCPUs {
cpus = append(cpus, strconv.FormatInt(id, 10))
}
forkQemuCmd = append(forkQemuCmd, "cpus="+strings.Join(cpus, ","))
}
for i := range fdFiles {
// Pass through any file descriptors as 3+i (as first 3 file descriptors are taken as standard).
forkQemuCmd = append(forkQemuCmd, fmt.Sprintf("fd=%d", 3+i))
}
// Ensure SELinux context is generated and persisted.
contextIsNew, err := d.selinuxEnsureContext()
if err != nil {
return err
}
seCtx := d.localConfig["volatile.selinux.context"]
if seCtx != "" {
forkQemuCmd = append(forkQemuCmd, "secontext="+seCtx)
}
// Log the QEMU command line.
fullCmd := append(forkQemuCmd, "--", qemuPath)
fullCmd = append(fullCmd, d.cmdArgs...)
d.logger.Debug("Starting QEMU", logger.Ctx{"command": fullCmd})
// Setup background process.
p, err := subprocess.NewProcess(d.state.OS.ExecPath, fullCmd, d.EarlyLogFilePath(), d.EarlyLogFilePath())
if err != nil {
op.Done(err)
return err
}
// Load the AppArmor profile
err = apparmor.InstanceLoad(d.state.OS, d, []string{qemuPath})
if err != nil {
op.Done(err)
return err
}
p.SetApparmor(apparmor.InstanceProfileName(d))
// Update the backup.yaml file just before starting the instance process, but after all devices have been
// setup, so that the backup file contains the volatile keys used for this instance start, so that they can
// be used for instance cleanup.
err = d.UpdateBackupFile()
if err != nil {
err = fmt.Errorf("Failed updating backup file: %w", err)
op.Done(err)
return err
}
err = p.StartWithFiles(context.Background(), fdFiles)
if err != nil {
op.Done(err)
return err
}
_, err = p.Wait(context.Background())
if err != nil {
stderr, _ := os.ReadFile(d.EarlyLogFilePath())
err = fmt.Errorf("Failed to run: %s: %s: %w", strings.Join(p.Args, " "), string(stderr), err)
op.Done(err)
return err
}
pid, err := d.pid()
if err != nil || pid <= 0 {
d.logger.Error("Failed to get VM process ID", logger.Ctx{"err": err, "pid": pid})
op.Done(err)
return err
}
reverter.Add(func() {
_ = d.killQemuProcess(pid)
})
// Start QMP monitoring.
monitor, err := d.qmpConnect()
if err != nil {
op.Done(err)
return err
}
// Record the QEMU machine definition.
// NOTE: We can't use CanLiveMigrate during instance startup as the boot state hasn't yet been recorded.
if !stateful && util.IsTrue(d.expandedConfig["migration.stateful"]) {
definition, err := monitor.MachineDefinition()
if err != nil {
op.Done(err)
return err
}
bs.MachineType = definition
}
// Don't allow the monitor to trigger a disconnection shutdown event until cleanly started so that the
// onStop hook isn't triggered prematurely (as this function's reverter will clean up on failure to start).
monitor.SetInitialized(false)
// Early startup hook
err = d.startupHook(monitor, "early")
if err != nil {
op.Done(err)
return err
}
// Apply CPU pinning.
if bs.CPUTopology.VCPUs == nil {
if d.architectureSupportsCPUHotplug() && !bs.CPUTopology.Explicit && bs.CPUTopology.Cores > 1 {
// Hotplug the CPUs.
err := d.setCPUs(monitor, bs.CPUTopology.Cores)
if err != nil {
err = fmt.Errorf("Failed to add CPUs: %w", err)
op.Done(err)
return err
}
}
} else {
// Get the list of PIDs from the VM.
pids, err := monitor.GetCPUs()
if err != nil {
op.Done(err)
return err
}
// Confirm nothing weird is going on.
if len(bs.CPUTopology.VCPUs) != len(pids) {
err = errors.New("QEMU has less vCPUs than configured")
op.Done(err)
return err
}
// Apply the CPU pins.
for i, pid := range pids {
set := unix.CPUSet{}
set.Set(int(bs.CPUTopology.VCPUs[uint64(i)]))
// Apply the pin.
err := unix.SchedSetaffinity(pid, &set)
if err != nil {
op.Done(err)
return err
}
}
// Create a core scheduling group.
err = d.setCoreSched(pids)
if err != nil {
err = fmt.Errorf("Failed to allocate new core scheduling domain for vCPU threads: %w", err)
op.Done(err)
return err
}
}
// After a constrained startup, let the non-vCPU threads spread across the system again.
if len(startupCPUs) > 0 {
err = d.resetSupportThreadsAffinity(monitor, pid)
if err != nil {
op.Done(err)
return err
}
}
// Run monitor hooks from devices.
for _, monHook := range monHooks {
err = monHook(monitor)
if err != nil {
op.Done(err)
return fmt.Errorf("Failed setting up device via monitor: %w", err)
}
}
// Pre-start startup hook
err = d.startupHook(monitor, "pre-start")
if err != nil {
op.Done(err)
return err
}
// Due to a bug in QEMU, devices added using QMP's device_add command do not have their bootindex option
// respected (even if added before emuation is started). To workaround this we must reset the VM in order
// for it to rebuild its boot config and to take into account the devices bootindex settings.
// This also means we cannot start the QEMU process with the -no-reboot flag, so we set the same reboot
// action below after this call.
err = monitor.Reset()
if err != nil {
op.Done(err)
return fmt.Errorf("Failed resetting VM: %w", err)
}
// Set our default actions. Those can still be overridden in the event handler when Incus is running.
actions := map[string]string{
"shutdown": "poweroff",
"reboot": "reset",
"panic": "exit-failure",
}
err = monitor.SetAction(actions)
if err != nil {
op.Done(err)
return fmt.Errorf("Failed setting reboot action: %w", err)
}
// Restore the state.
if stateful {
// Add back any memory hotplug slot.
if bs.MemoryTopology != nil {
for _, memSize := range bs.MemoryTopology.Extra {
err := d.hotplugMemory(monitor, memSize)
if err != nil {
return err
}
}
}
// Receive the state.
err = d.restoreState(monitor)
if err != nil {
op.Done(err)
return err
}
}
// Label rootfs if SELinux context is set and labels are missing.
err = d.selinuxLabelFiles(contextIsNew)
if err != nil {
return err
}
// Start the VM.
err = monitor.Start()
if err != nil {
err = fmt.Errorf("Failed starting VM: %w", err)
op.Done(err)
return err
}
// Finish handling stateful start.
if stateful {
// Cleanup state.
_ = os.Remove(d.StatePath())
d.stateful = false
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
return tx.UpdateInstanceStatefulFlag(ctx, d.id, false)
})
if err != nil {
op.Done(err)
return fmt.Errorf("Error updating instance stateful flag: %w", err)
}
}
// Record last start state.
err = d.recordLastState()
if err != nil {
op.Done(err)
return err
}
reverter.Success()
// Post-start startup hook
err = d.startupHook(monitor, "post-start")
if err != nil {
op.Done(err)
// Shut down the VM if the post-start commands fail.
_ = d.Stop(false)
return err
}
// Run any post-start hooks.
err = d.runHooks(postStartHooks)
if err != nil {
op.Done(err) // Must come before Stop() otherwise stop will not proceed.
// Shut down the VM if hooks fail.
_ = d.Stop(false)
return err
}
// Apply OOM priority after container is started and hooks completed.
err = d.setOOMPriority(d.InitPID())
if err != nil {
d.logger.Warn("Failed to set OOM priority", logger.Ctx{
"err": err,
"instance": d.Name(),
"project": d.Project().Name,
})
}
if op.Action() == "start" {
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceStarted.Event(d, nil))
}
// The VM started cleanly so now enable the unexpected disconnection event to ensure the onStop hook is
// run if QMP unexpectedly disconnects.
monitor.SetInitialized(true)
op.Done(nil)
// Record the final boot state data.
err = d.saveBootState(*bs)
if err != nil {
return err
}
return nil
}
func (d *qemu) setupSEV(fdFiles *[]*os.File) (*qemuSevOpts, error) {
if d.architecture != osarch.ARCH_64BIT_INTEL_X86 {
return nil, errors.New("AMD SEV support is only available on x86_64 systems")
}
// Get the QEMU features to check if AMD SEV is supported.
info := DriverStatuses()[instancetype.VM].Info
_, smeFound := info.Features["sme"] // codespell:ignore sme
sev, sevFound := info.Features["sev"]
if !smeFound || !sevFound {
return nil, errors.New("AMD SEV is not supported by the host")
}
// Get the SEV guest `cbitpos` and `reducedPhysBits`.
sevCapabilities, ok := sev.(qmp.AMDSEVCapabilities)
if !ok {
return nil, errors.New(`Failed to get the guest "sev" capabilities`)
}
cbitpos := sevCapabilities.CBitPos
reducedPhysBits := sevCapabilities.ReducedPhysBits
// Write user's dh-cert and session-data to file descriptors.
var dhCertFD, sessionDataFD int
if d.expandedConfig["security.sev.session.dh"] != "" {
dhCert, err := os.CreateTemp("", "incus_sev_dh_cert_")
if err != nil {
return nil, err
}
err = os.Remove(dhCert.Name())
if err != nil {
return nil, err
}
_, err = dhCert.WriteString(d.expandedConfig["security.sev.session.dh"])
if err != nil {
return nil, err
}
dhCertFD = d.addFileDescriptor(fdFiles, dhCert)
}
if d.expandedConfig["security.sev.session.data"] != "" {
sessionData, err := os.CreateTemp("", "incus_sev_session_data_")
if err != nil {
return nil, err
}
err = os.Remove(sessionData.Name())
if err != nil {
return nil, err
}
_, err = sessionData.WriteString(d.expandedConfig["security.sev.session.data"])
if err != nil {
return nil, err
}
sessionDataFD = d.addFileDescriptor(fdFiles, sessionData)
}
sevOpts := &qemuSevOpts{}
sevOpts.cbitpos = cbitpos
sevOpts.reducedPhysBits = reducedPhysBits
if dhCertFD > 0 && sessionDataFD > 0 {
sevOpts.dhCertFD = fmt.Sprintf("/proc/self/fd/%d", dhCertFD)
sevOpts.sessionDataFD = fmt.Sprintf("/proc/self/fd/%d", sessionDataFD)
}
if util.IsTrue(d.expandedConfig["security.sev.policy.es"]) {
_, sevES := info.Features["sev-es"]
if !sevES {
return nil, errors.New("AMD SEV-ES is not supported by the host")
}
// This bit mask is used to specify a guest policy. '0x5' is for SEV-ES. The details of the available policies can be found in the link below (see chapter 3)
// https://www.amd.com/system/files/TechDocs/55766_SEV-KM_API_Specification.pdf
sevOpts.policy = "0x5"
} else {
// '0x1' is for a regular SEV policy.
sevOpts.policy = "0x1"
}
return sevOpts, nil
}
// getAgentConnectionInfo returns the connection info the agent needs to connect to the server.
func (d *qemu) getAgentConnectionInfo() (*agentAPI.API10Put, error) {
addr := d.state.Endpoints.VsockAddress()
if addr == nil {
return nil, nil
}
vsockaddr, ok := addr.(*vsock.Addr)
if !ok {
return nil, errors.New("Listen address is not vsock.Addr")
}
req := agentAPI.API10Put{
Certificate: string(d.state.Endpoints.NetworkCert().PublicKey()),
DevIncus: util.IsTrueOrEmpty(d.expandedConfig["security.guestapi"]),
CID: vsock.Host, // Always tell the agent to connect to the server using the Host Context ID to support nesting.
Port: vsockaddr.Port,
}
return &req, nil
}
// advertiseVsockAddress advertises the CID and port to the VM.
func (d *qemu) advertiseVsockAddress() error {
client, err := d.getAgentClient()
if err != nil {
return fmt.Errorf("Failed getting agent client handle: %w", err)
}
agentArgs := &incus.ConnectionArgs{
SkipGetEvents: true,
SkipGetServer: true,
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
agent, err := incus.ConnectIncusHTTPWithContext(ctx, agentArgs, client)
if err != nil {
return fmt.Errorf("Failed connecting to the agent: %w", err)
}
defer agent.Disconnect()
connInfo, err := d.getAgentConnectionInfo()
if err != nil {
return err
}
if connInfo == nil {
return nil
}
_, _, err = agent.RawQuery("PUT", "/1.0", connInfo, "")
if err != nil {
return fmt.Errorf("Failed sending host vsock information to the agent: %w", err)
}
return nil
}
// AgentCertificate returns the server certificate of the agent.
func (d *qemu) AgentCertificate() *x509.Certificate {
agentCert := filepath.Join(d.Path(), "config", "agent.crt")
if !util.PathExists(agentCert) {
return nil
}
cert, err := localtls.ReadCert(agentCert)
if err != nil {
return nil
}
return cert
}
func (d *qemu) architectureSupportsUEFI(arch int) bool {
return slices.Contains([]int{osarch.ARCH_64BIT_INTEL_X86, osarch.ARCH_64BIT_ARMV8_LITTLE_ENDIAN}, arch)
}
// firmwarePairs returns the candidate firmware pairs for the instance based on its configuration.
// When AMD SEV is enabled, a SEV-capable firmware is preferred but falls back to the regular firmware
// (GENERIC, SECUREBOOT or CSM) if no SEV build is available.
func (d *qemu) firmwarePairs() ([]edk2.FirmwarePair, error) {
if util.IsTrue(d.expandedConfig["security.sev"]) {
firmwares, err := edk2.GetArchitectureFirmwarePairsForUsage(d.architecture, edk2.SEV)
if err != nil {
return nil, err
}
if len(firmwares) > 0 {
return firmwares, nil
}
}
switch {
case util.IsTrue(d.expandedConfig["security.csm"]):
return edk2.GetArchitectureFirmwarePairsForUsage(d.architecture, edk2.CSM)
case util.IsTrueOrEmpty(d.expandedConfig["security.secureboot"]):
return edk2.GetArchitectureFirmwarePairsForUsage(d.architecture, edk2.SECUREBOOT)
default:
return edk2.GetArchitectureFirmwarePairsForUsage(d.architecture, edk2.GENERIC)
}
}
func (d *qemu) setupNvram() error {
var err error
d.logger.Debug("Generating NVRAM")
// Cleanup existing variables.
firmwares, err := edk2.GetArchitectureFirmwarePairs(d.architecture)
if err != nil {
return err
}
for _, firmwarePair := range firmwares {
err := os.Remove(filepath.Join(d.Path(), filepath.Base(firmwarePair.Vars)))
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
}
// Determine expected firmware.
firmwares, err = d.firmwarePairs()
if err != nil {
return err
}
// Unified firmware images (e.g. AMD SEV) carry their own variable store and need no NVRAM.
needsNvram := false
for _, firmware := range firmwares {
if firmware.Vars != "" {
needsNvram = true
break
}
}
if !needsNvram {
return nil
}
// Find the template file.
var efiVarsPath string
var efiVarsName string
for _, firmware := range firmwares {
if firmware.Vars == "" {
continue
}
varsPath, err := filepath.EvalSymlinks(firmware.Vars)
if err != nil {
continue
}
if util.PathExists(varsPath) {
efiVarsPath = varsPath
efiVarsName = filepath.Base(firmware.Vars)
break
}
}
if efiVarsPath == "" {
return fmt.Errorf("Couldn't find one of the required UEFI firmware files: %+v", firmwares)
}
// Copy the template.
err = internalUtil.FileCopy(efiVarsPath, filepath.Join(d.Path(), efiVarsName))
if err != nil {
return err
}
nvramPath := d.nvramPath()
// Handle the case where the firmware vars filename matches our internal one.
if efiVarsName == filepath.Base(nvramPath) {
return nil
}
// Generate a symlink.
// This is so qemu.nvram can always be assumed to be the EDK2 vars file.
// The real file name is then used to determine what firmware must be selected.
_ = os.Remove(nvramPath)
err = os.Symlink(efiVarsName, nvramPath)
if err != nil {
return err
}
return nil
}
func (d *qemu) qemuArchConfig(arch int) (string, string, error) {
var bus string
var qemuCmd string
switch arch {
case osarch.ARCH_64BIT_INTEL_X86:
qemuCmd = "qemu-system-x86_64"
bus = "pcie"
case osarch.ARCH_64BIT_ARMV8_LITTLE_ENDIAN:
qemuCmd = "qemu-system-aarch64"
bus = "pcie"
case osarch.ARCH_64BIT_POWERPC_LITTLE_ENDIAN:
qemuCmd = "qemu-system-ppc64"
bus = "pci"
case osarch.ARCH_64BIT_S390_BIG_ENDIAN:
qemuCmd = "qemu-system-s390x"
bus = "ccw"
default:
return "", "", errors.New("Architecture isn't supported for virtual machines")
}
qemuPath, err := exec.LookPath(qemuCmd)
if err != nil {
hostArch, archErr := osarch.ArchitectureGetLocalID()
if archErr != nil {
return "", "", err
}
if arch == hostArch && util.PathExists("/usr/libexec/qemu-kvm") {
return "/usr/libexec/qemu-kvm", bus, nil
}
return "", "", err
}
return qemuPath, bus, nil
}
// RegisterDevices calls the Register() function on all of the instance's devices.
func (d *qemu) RegisterDevices() {
d.devicesRegister(d)
}
func (d *qemu) saveConnectionInfo(connInfo *agentAPI.API10Put) error {
configDrivePath := filepath.Join(d.Path(), "config")
f, err := os.Create(filepath.Join(configDrivePath, "agent.conf"))
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()
err = json.NewEncoder(f).Encode(connInfo)
if err != nil {
return err
}
return nil
}
// OnHook is the top-level hook handler.
func (d *qemu) OnHook(hookName string, args map[string]string) error {
return instance.ErrNotImplemented
}
// deviceStart loads a new device and calls its Start() function.
func (d *qemu) deviceStart(dev device.Device, instanceRunning bool) (*deviceConfig.RunConfig, error) {
configCopy := dev.Config()
l := d.logger.AddContext(logger.Ctx{"device": dev.Name(), "type": configCopy["type"]})
l.Debug("Starting device")
reverter := revert.New()
defer reverter.Fail()
if instanceRunning && !dev.CanHotPlug() {
return nil, errors.New("Device cannot be started when instance is running")
}
runConf, err := dev.Start()
if err != nil {
return nil, err
}
reverter.Add(func() {
runConf, _ := dev.Stop()
if runConf != nil {
_ = d.runHooks(runConf.PostHooks)
}
})
// If runConf supplied, perform any instance specific setup of device.
if runConf != nil {
// If instance is running and then live attach device.
if instanceRunning {
// Attach NIC to running instance.
if len(runConf.NetworkInterface) > 0 {
err = d.deviceAttachNIC(dev.Name(), configCopy, runConf)
if err != nil {
return nil, err
}
}
// Attach disk to running instance.
for _, mount := range runConf.Mounts {
if mount.FSType == "9p" {
err = d.deviceAttachPath(dev.Name(), configCopy, mount)
if err != nil {
return nil, err
}
} else if mount.TargetPath != "/" {
err = d.deviceAttachBlockDevice(dev.Name(), configCopy, mount)
if err != nil {
return nil, err
}
}
}
// Attach USB to running instance.
for _, usbDev := range runConf.USBDevice {
err = d.deviceAttachUSB(usbDev)
if err != nil {
return nil, err
}
}
// Attach PCI to running instance.
if len(runConf.PCIDevice) > 0 {
err = d.deviceAttachPCI(dev.Name(), configCopy, runConf.PCIDevice)
if err != nil {
return nil, err
}
}
// If running, run post start hooks now (if not, they will be run
// once the instance is started).
err = d.runHooks(runConf.PostHooks)
if err != nil {
return nil, err
}
}
}
reverter.Success()
return runConf, nil
}
func (d *qemu) deviceAttachPath(deviceName string, configCopy map[string]string, mount deviceConfig.MountEntryItem) error {
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return fmt.Errorf("Failed to connect to QMP monitor: %w", err)
}
monHook, err := d.addDriveDirConfigVirtiofs(nil, nil, mount)
if err != nil {
return fmt.Errorf("Failed to add drive config: %w", err)
}
err = monHook(monitor)
if err != nil {
return fmt.Errorf("Failed to call monitor hook for block device: %w", err)
}
return nil
}
func (d *qemu) deviceAttachBlockDevice(deviceName string, configCopy map[string]string, mount deviceConfig.MountEntryItem) error {
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return fmt.Errorf("Failed to connect to QMP monitor: %w", err)
}
monHook, err := d.addDriveConfig(nil, nil, mount)
if err != nil {
return fmt.Errorf("Failed to add drive config: %w", err)
}
err = monHook(monitor)
if err != nil {
return fmt.Errorf("Failed to call monitor hook for block device: %w", err)
}
return nil
}
func (d *qemu) deviceDetachPath(deviceName string, rawConfig deviceConfig.Device) error {
escapedDeviceName := linux.PathNameEncode(deviceName)
deviceID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName)
mountTag := d.mountTagName(deviceName, qemuMountTagMaxLength)
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
err = monitor.RemoveDevice(deviceID)
if err != nil {
return err
}
waitDuration := time.Duration(time.Second * time.Duration(10))
waitUntil := time.Now().Add(waitDuration)
for {
err = monitor.RemoveCharDevice(mountTag)
if err == nil {
break
}
if api.StatusErrorCheck(err, http.StatusLocked) {
time.Sleep(time.Second * time.Duration(2))
continue
}
if time.Now().After(waitUntil) {
return fmt.Errorf("Failed to detach path device after %v", waitDuration)
}
}
return nil
}
func (d *qemu) deviceDetachBlockDevice(deviceName string, rawConfig deviceConfig.Device) error {
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
escapedDeviceName := linux.PathNameEncode(deviceName)
deviceID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName)
blockDevName := d.blockNodeName(escapedDeviceName)
blockDevs, err := d.fetchBlockDeviceChain(monitor, blockDevName)
if err != nil {
return err
}
for i := len(blockDevs); i > 0; i-- {
blockDev := blockDevs[i-1]
err = monitor.RemoveFDFromFDSet(blockDev)
if err != nil {
return err
}
}
err = monitor.RemoveDevice(deviceID)
if err != nil {
return err
}
if rawConfig["io.bus"] == "usb" {
// When dealing with USB, remove the intermediate USB device too.
err = monitor.RemoveDevice("usb_" + deviceID)
if err != nil {
return err
}
}
for i := len(blockDevs); i > 0; i-- {
blockDev := blockDevs[i-1]
err = d.detachBlockDeviceAndWait(monitor, blockDev)
if err != nil {
return err
}
}
return nil
}
func (d *qemu) detachBlockDeviceAndWait(m *qmp.Monitor, blockDevName string) error {
waitDuration := time.Duration(time.Second * time.Duration(10))
waitUntil := time.Now().Add(waitDuration)
for {
err := m.RemoveBlockDevice(blockDevName)
if err == nil {
break
}
if api.StatusErrorCheck(err, http.StatusLocked) {
time.Sleep(time.Second * time.Duration(2))
continue
}
if time.Now().After(waitUntil) {
return fmt.Errorf("Failed to detach block device after %v", waitDuration)
}
}
return nil
}
// deviceAttachNIC live attaches a NIC device to the instance.
func (d *qemu) deviceAttachNIC(deviceName string, configCopy map[string]string, runConf *deviceConfig.RunConfig) error {
devName := ""
for _, dev := range runConf.NetworkInterface {
if dev.Key == "link" {
devName = dev.Value
break
}
}
if devName == "" {
return errors.New("Device didn't provide a link property to use")
}
_, qemuBus, err := d.qemuArchConfig(d.architecture)
if err != nil {
return err
}
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
qemuDev := make(map[string]any)
if runConf.UseUSBBus {
qemuBus = "usb"
qemuDev["bus"] = "qemu_usb.0"
} else if slices.Contains([]string{"pcie", "pci"}, qemuBus) {
// Try to get a PCI address for hotplugging.
pciDeviceName, err := d.getPCIHotplug()
if err != nil {
return err
}
d.logger.Debug("Using PCI bus device to hotplug NIC into", logger.Ctx{"device": deviceName, "port": pciDeviceName})
qemuDev["bus"] = pciDeviceName
qemuDev["addr"] = "00.0"
}
monHook, err := d.addNetDevConfig(qemuBus, qemuDev, nil, runConf.NetworkInterface)
if err != nil {
return err
}
err = monHook(monitor)
if err != nil {
return err
}
return nil
}
func (d *qemu) getPCIHotplug() (string, error) {
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return "", err
}
// Get the current PCI devices.
devices, err := monitor.QueryPCI()
if err != nil {
return "", err
}
for _, dev := range devices {
// Skip built-in devices.
if dev.DevID == "" || dev.DevID == "qemu_iommu" {
continue
}
// Skip used bridges.
if len(dev.Bridge.Devices) > 0 {
continue
}
// Found an empty slot.
return dev.DevID, nil
}
return "", errors.New("No available PCI hotplug slots could be found")
}
// deviceAttachPCI live attaches a generic PCI device to the instance.
func (d *qemu) deviceAttachPCI(deviceName string, configCopy map[string]string, pciConfig []deviceConfig.RunConfigItem) error {
reverter := revert.New()
defer reverter.Fail()
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
// Get the device config.
var devName, pciSlotName, pciIOMMUGroup string
for _, pciItem := range pciConfig {
switch pciItem.Key {
case "devName":
devName = pciItem.Value
case "pciSlotName":
pciSlotName = pciItem.Value
case "pciIOMMUGroup":
pciIOMMUGroup = pciItem.Value
}
}
// PCIe and PCI require a port device name to hotplug the NIC into.
_, qemuBus, err := d.qemuArchConfig(d.architecture)
if err != nil {
return err
}
if !slices.Contains([]string{"pcie", "pci"}, qemuBus) {
return errors.New("Attempting PCI passthrough on a non-PCI system")
}
// Try to get a PCI address for hotplugging.
pciDeviceName, err := d.getPCIHotplug()
if err != nil {
return err
}
qemuDev := make(map[string]any)
escapedDeviceName := linux.PathNameEncode(devName)
d.logger.Debug("Using PCI bus device to hotplug NIC into", logger.Ctx{"device": deviceName, "port": pciDeviceName})
qemuDev["bus"] = pciDeviceName
qemuDev["addr"] = "00.0"
qemuDev["driver"] = "vfio-pci"
qemuDev["id"] = fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName)
qemuDev["host"] = pciSlotName
if d.state.OS.UnprivUser != "" {
if pciIOMMUGroup == "" {
return errors.New("No PCI IOMMU group supplied")
}
vfioGroupFile := fmt.Sprintf("/dev/vfio/%s", pciIOMMUGroup)
err := os.Chown(vfioGroupFile, int(d.state.OS.UnprivUID), -1)
if err != nil {
return fmt.Errorf("Failed to chown vfio group device %q: %w", vfioGroupFile, err)
}
}
err = monitor.AddDevice(qemuDev)
if err != nil {
return fmt.Errorf("Failed setting up device %q: %w", devName, err)
}
return nil
}
// deviceStop loads a new device and calls its Stop() function.
func (d *qemu) deviceStop(dev device.Device, instanceRunning bool, _ string) error {
configCopy := dev.Config()
l := d.logger.AddContext(logger.Ctx{"device": dev.Name(), "type": configCopy["type"]})
l.Debug("Stopping device")
if instanceRunning && !dev.CanHotPlug() {
return errors.New("Device cannot be stopped when instance is running")
}
// The root disk device can't be hot-unplugged.
if instanceRunning && internalInstance.IsRootDiskDevice(configCopy) {
return errors.New("Root disk device cannot be detached from a running instance")
}
runConf, err := dev.Stop()
if err != nil {
return err
}
if instanceRunning {
// Detach NIC from running instance.
if configCopy["type"] == "nic" {
for _, usbDev := range runConf.USBDevice {
err = d.deviceDetachUSB(usbDev)
if err != nil {
return err
}
}
err = d.deviceDetachNIC(dev.Name())
if err != nil {
return err
}
}
// Detach USB from running instance.
if configCopy["type"] == "usb" && runConf != nil {
for _, usbDev := range runConf.USBDevice {
err = d.deviceDetachUSB(usbDev)
if err != nil {
return err
}
}
}
// Detach disk from running instance.
if configCopy["type"] == "disk" {
if configCopy["path"] != "" {
err = d.deviceDetachPath(dev.Name(), configCopy)
if err != nil {
return err
}
} else {
err = d.deviceDetachBlockDevice(dev.Name(), configCopy)
if err != nil {
return err
}
}
}
// Detach generic PCI device from running instance.
if configCopy["type"] == "pci" {
err = d.deviceDetachPCI(dev.Name())
if err != nil {
return err
}
}
}
if runConf != nil {
// Run post stop hooks irrespective of run state of instance.
err = d.runHooks(runConf.PostHooks)
if err != nil {
return err
}
}
return nil
}
// deviceDetachNIC detaches a NIC device from a running instance.
func (d *qemu) deviceDetachNIC(deviceName string) error {
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
escapedDeviceName := linux.PathNameEncode(deviceName)
deviceID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName)
netDevID := fmt.Sprintf("%s%s", qemuNetDevIDPrefix, escapedDeviceName)
// Request removal of device.
err = monitor.RemoveDevice(deviceID)
if err != nil {
return fmt.Errorf("Failed removing NIC device: %w", err)
}
err = monitor.RemoveNIC(netDevID)
if err != nil {
return err
}
_, qemuBus, err := d.qemuArchConfig(d.architecture)
if err != nil {
return err
}
if slices.Contains([]string{"pcie", "pci"}, qemuBus) {
// Wait until the device is actually removed (or we timeout waiting).
waitDuration := time.Duration(time.Second * time.Duration(10))
waitUntil := time.Now().Add(waitDuration)
for {
devExists, err := monitor.CheckPCIDevice(deviceID)
if err != nil {
return fmt.Errorf("Failed getting PCI devices to check for NIC detach: %w", err)
}
if !devExists {
break
}
if time.Now().After(waitUntil) {
return fmt.Errorf("Failed to detach NIC after %v", waitDuration)
}
d.logger.Debug("Waiting for NIC device to be detached", logger.Ctx{"device": deviceName})
time.Sleep(time.Second * time.Duration(2))
}
}
return nil
}
// deviceDetachPCI detaches a generic PCI device from a running instance.
func (d *qemu) deviceDetachPCI(deviceName string) error {
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
escapedDeviceName := linux.PathNameEncode(deviceName)
deviceID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName)
// Request removal of device.
err = monitor.RemoveDevice(deviceID)
if err != nil {
return fmt.Errorf("Failed removing PCI device: %w", err)
}
_, qemuBus, err := d.qemuArchConfig(d.architecture)
if err != nil {
return err
}
if slices.Contains([]string{"pcie", "pci"}, qemuBus) {
// Wait until the device is actually removed (or we timeout waiting).
waitDuration := time.Duration(time.Second * time.Duration(10))
waitUntil := time.Now().Add(waitDuration)
for {
devExists, err := monitor.CheckPCIDevice(deviceID)
if err != nil {
return fmt.Errorf("Failed getting PCI devices to check for detach: %w", err)
}
if !devExists {
break
}
if time.Now().After(waitUntil) {
return fmt.Errorf("Failed to detach PCI device after %v", waitDuration)
}
d.logger.Debug("Waiting for PCI device to be detached", logger.Ctx{"device": deviceName})
time.Sleep(time.Second * time.Duration(2))
}
}
return nil
}
func (d *qemu) monitorPath() string {
return filepath.Join(d.RunPath(), "qemu.monitor")
}
func (d *qemu) nvramPath() string {
return filepath.Join(d.Path(), "qemu.nvram")
}
func (d *qemu) consolePath() string {
return filepath.Join(d.RunPath(), "qemu.console")
}
func (d *qemu) spicePath() string {
return filepath.Join(d.RunPath(), "qemu.spice")
}
func (d *qemu) nbdPath() string {
return filepath.Join(d.RunPath(), "qemu.nbd")
}
func (d *qemu) migrateSockPath() string {
return filepath.Join(d.RunPath(), "migrate.sock")
}
func (d *qemu) spiceCmdlineConfig(fdFiles *[]*os.File) (string, error) {
// Reference the socket through a short /proc/self/fd path to handle
// run paths that exceed the unix socket path limit.
spiceDir, err := os.OpenFile(d.RunPath(), unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return "", err
}
spiceDirFD := d.addFileDescriptor(fdFiles, spiceDir)
spicePath := fmt.Sprintf("/proc/self/fd/%d/qemu.spice", spiceDirFD)
return fmt.Sprintf("unix=on,disable-ticketing=on,addr=%s", spicePath), nil
}
// generateConfigShare generates the config share directory that will be exported to the VM via
// a 9P share. Due to the unknown size of templates inside the images this directory is created
// inside the VM's config volume so that it can be restricted by quota.
// Requires the instance be mounted before calling this function.
func (d *qemu) generateConfigShare(volatileSet map[string]string) error {
configDrivePath := filepath.Join(d.Path(), "config")
// Create config drive dir if doesn't exist, if it does exist, leave it around so we don't regenerate all
// files causing unnecessary config drive snapshot usage.
err := os.MkdirAll(configDrivePath, 0o500)
if err != nil {
return err
}
guestOS := d.GuestOS()
if guestOS == osinfo.UnknownOS {
guestOS = osinfo.Linux
}
// Windows doesn't handle shares.
if guestOS != osinfo.Windows {
// Add the VM agent loader.
agentSrcPath, _ := exec.LookPath("incus-agent")
if util.PathExists(os.Getenv("INCUS_AGENT_PATH")) {
// Install incus-agent script (loads from agent share).
agentFile, err := incusAgentLoader.ReadFile("agent-loader/incus-agent-" + string(guestOS))
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "incus-agent"), agentFile, 0o700)
if err != nil {
return err
}
// Legacy support.
_ = os.Remove(filepath.Join(configDrivePath, "lxd-agent"))
err = os.Symlink("incus-agent", filepath.Join(configDrivePath, "lxd-agent"))
if err != nil {
return err
}
} else if agentSrcPath != "" {
// Install agent into config drive dir if found.
agentSrcPath, err = filepath.EvalSymlinks(agentSrcPath)
if err != nil {
return err
}
agentSrcInfo, err := os.Stat(agentSrcPath)
if err != nil {
return fmt.Errorf("Failed getting info for incus-agent source %q: %w", agentSrcPath, err)
}
agentInstallPath := filepath.Join(configDrivePath, "incus-agent")
agentNeedsInstall := true
if util.PathExists(agentInstallPath) {
agentInstallInfo, err := os.Stat(agentInstallPath)
if err != nil {
return fmt.Errorf("Failed getting info for existing incus-agent install %q: %w", agentInstallPath, err)
}
if agentInstallInfo.ModTime().Equal(agentSrcInfo.ModTime()) && agentInstallInfo.Size() == agentSrcInfo.Size() {
agentNeedsInstall = false
}
}
// Only install the agent into config drive if the existing one is different to the source one.
// Otherwise we would end up copying it again and this can cause unnecessary snapshot usage.
if agentNeedsInstall {
d.logger.Debug("Installing incus-agent", logger.Ctx{"srcPath": agentSrcPath, "installPath": agentInstallPath})
err = internalUtil.FileCopy(agentSrcPath, agentInstallPath)
if err != nil {
return err
}
err = os.Chmod(agentInstallPath, 0o500)
if err != nil {
return err
}
err = os.Chown(agentInstallPath, 0, 0)
if err != nil {
return err
}
// Ensure we copy the source file's timestamps so they can be used for comparison later.
err = os.Chtimes(agentInstallPath, agentSrcInfo.ModTime(), agentSrcInfo.ModTime())
if err != nil {
return fmt.Errorf("Failed setting incus-agent timestamps: %w", err)
}
} else {
d.logger.Debug("Skipping incus-agent install as unchanged", logger.Ctx{"srcPath": agentSrcPath, "installPath": agentInstallPath})
}
// Legacy support.
_ = os.Remove(filepath.Join(configDrivePath, "lxd-agent"))
err = os.Symlink("incus-agent", filepath.Join(configDrivePath, "lxd-agent"))
if err != nil {
return err
}
} else {
d.logger.Warn("incus-agent not found, skipping its inclusion in the VM config drive", logger.Ctx{"err": err})
}
}
agentCert, agentKey, clientCert, _, err := d.generateAgentCert()
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "server.crt"), []byte(clientCert), 0o400)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "agent.crt"), []byte(agentCert), 0o400)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "agent.key"), []byte(agentKey), 0o400)
if err != nil {
return err
}
// OS-specific configuration.
switch guestOS {
case osinfo.FreeBSD:
// rc.d service.
err = os.MkdirAll(filepath.Join(configDrivePath, "rc.d"), 0o500)
if err != nil {
return err
}
// rc.d service for incus-agent.
agentFile, err := incusAgentLoader.ReadFile("agent-loader/rc.d/incus-agent")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "rc.d", "incus-agent"), agentFile, 0o500)
if err != nil {
return err
}
// Setup script for incus-agent that is executed by the incus-agent service before starting.
// The script sets up a temporary mount point, copies data from the mount (including incus-agent binary),
// and then unmounts it. It also ensures appropriate permissions for the Incus agent's runtime directory.
agentFile, err = incusAgentLoader.ReadFile("agent-loader/incus-agent-setup-freebsd")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "incus-agent-setup"), agentFile, 0o500)
if err != nil {
return err
}
// Install script for manual installs.
agentFile, err = incusAgentLoader.ReadFile("agent-loader/install-freebsd.sh")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "install.sh"), agentFile, 0o500)
if err != nil {
return err
}
case osinfo.Linux:
// Systemd units.
err = os.MkdirAll(filepath.Join(configDrivePath, "systemd"), 0o500)
if err != nil {
return err
}
// Systemd unit for incus-agent. It ensures the incus-agent is copied from the shared filesystem before it is
// started. The service is triggered dynamically via udev rules when certain virtio-ports are detected,
// rather than being enabled at boot.
agentFile, err := incusAgentLoader.ReadFile("agent-loader/systemd/incus-agent.service")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "systemd", "incus-agent.service"), agentFile, 0o400)
if err != nil {
return err
}
// Setup script for incus-agent that is executed by the incus-agent systemd unit before incus-agent is started.
// The script sets up a temporary mount point, copies data from the mount (including incus-agent binary),
// and then unmounts it. It also ensures appropriate permissions for the Incus agent's runtime directory.
agentFile, err = incusAgentLoader.ReadFile("agent-loader/incus-agent-setup-linux")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "systemd", "incus-agent-setup"), agentFile, 0o500)
if err != nil {
return err
}
err = os.MkdirAll(filepath.Join(configDrivePath, "udev"), 0o500)
if err != nil {
return err
}
// Udev rules to start the incus-agent.service when QEMU serial devices (symlinks in virtio-ports) appear.
agentFile, err = incusAgentLoader.ReadFile("agent-loader/systemd/incus-agent.rules")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "udev", "99-incus-agent.rules"), agentFile, 0o400)
if err != nil {
return err
}
// Install script for manual installs.
agentFile, err = incusAgentLoader.ReadFile("agent-loader/install-linux.sh")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "install.sh"), agentFile, 0o700)
if err != nil {
return err
}
case osinfo.MacOS:
// Launchd daemons.
err = os.MkdirAll(filepath.Join(configDrivePath, "launchd"), 0o500)
if err != nil {
return err
}
// Launchd daemon for incus-agent.
agentFile, err := incusAgentLoader.ReadFile("agent-loader/launchd/org.linuxcontainers.incus.macos-agent.plist")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "launchd", "org.linuxcontainers.incus.macos-agent.plist"), agentFile, 0o644)
if err != nil {
return err
}
// Setup script for incus-agent that is executed by launchd. For convenience, this script also
// launches the agent. Because of Apple TCC, sh must be given full disk access in the relevant
// system settings page. Other than that, this agent behaves roughly the same as the Linux one.
agentFile, err = incusAgentLoader.ReadFile("agent-loader/incus-agent-setup-macos")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "incus-agent-setup"), agentFile, 0o500)
if err != nil {
return err
}
// Install script for manual installs.
agentFile, err = incusAgentLoader.ReadFile("agent-loader/install-macos.sh")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "install.sh"), agentFile, 0o700)
if err != nil {
return err
}
case osinfo.Windows:
// Setup script for incus-agent that is executed by Service Control Manager (SCM). Since by
// default Windows cannot run a PowerShell script as a service without the help of a third
// party, a bat file is used to then execute the PowerShell script doing the job.
agentFile, err := incusAgentLoader.ReadFile("agent-loader/incus-agent-setup.ps1")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "incus-agent-setup.ps1"), agentFile, 0o500)
if err != nil {
return err
}
// Install script for manual installs.
agentFile, err = incusAgentLoader.ReadFile("agent-loader/install.ps1")
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(configDrivePath, "install.ps1"), agentFile, 0o700)
if err != nil {
return err
}
}
// Templated files.
templateFilesPath := filepath.Join(configDrivePath, "files")
// Clear path and recreate.
_ = os.RemoveAll(templateFilesPath)
err = os.MkdirAll(templateFilesPath, 0o500)
if err != nil {
return err
}
// Template anything that needs templating.
key := "volatile.apply_template"
if d.localConfig[key] != "" {
// Run any template that needs running.
err = d.templateApplyNow(instance.TemplateTrigger(d.localConfig[key]), templateFilesPath)
if err != nil {
return err
}
// Record that the instance devices got modified and a full reset will be needed to get a consistent state.
volatileSet[key] = ""
volatileSet["volatile.vm.needs_reset"] = "true"
}
err = d.templateApplyNow("start", templateFilesPath)
if err != nil {
return err
}
// Copy the template metadata itself too.
metaPath := filepath.Join(d.Path(), "metadata.yaml")
if util.PathExists(metaPath) {
err = internalUtil.FileCopy(metaPath, filepath.Join(templateFilesPath, "metadata.yaml"))
if err != nil {
return err
}
}
// Only Linux guests support dynamic NIC configuration.
if guestOS == osinfo.Linux {
// Clear NICConfigDir to ensure that no leftover configuration is erroneously applied by the agent.
nicConfigPath := filepath.Join(configDrivePath, deviceConfig.NICConfigDir)
_ = os.RemoveAll(nicConfigPath)
err = os.MkdirAll(nicConfigPath, 0o500)
if err != nil {
return err
}
// Add the NIC config.
if util.IsTrue(d.expandedConfig["agent.nic_config"]) {
sortedDevices := d.expandedDevices.Sorted()
for _, entry := range sortedDevices {
if entry.Config["type"] != "nic" {
continue // Only keep NIC devices.
}
dev, err := d.FillNetworkDevice(entry.Name, entry.Config)
if err != nil {
return err
}
err = d.writeNICDevConfig(dev["mtu"], entry.Name, dev["name"], dev["hwaddr"])
if err != nil {
return fmt.Errorf("Failed writing NIC config for device %q: %w", entry.Name, err)
}
}
}
// Writing the connection info the config drive allows the agent to start /dev/incus very
// early. This is important for systemd services which want or require /dev/incus/sock.
connInfo, err := d.getAgentConnectionInfo()
if err != nil {
return err
}
if connInfo != nil {
err = d.saveConnectionInfo(connInfo)
if err != nil {
return err
}
}
}
return nil
}
func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) error {
// If there's no metadata, just return.
fname := filepath.Join(d.Path(), "metadata.yaml")
if !util.PathExists(fname) {
return nil
}
// Parse the metadata.
content, err := os.ReadFile(fname)
if err != nil {
return fmt.Errorf("Failed to read metadata: %w", err)
}
metadata := &api.ImageMetadata{}
err = yaml.Load(content, metadata)
if err != nil {
return fmt.Errorf("Could not parse %s: %w", fname, err)
}
// Figure out the instance architecture.
arch, err := osarch.ArchitectureName(d.architecture)
if err != nil {
arch, err = osarch.ArchitectureName(d.state.OS.Architectures[0])
if err != nil {
return fmt.Errorf("Failed to detect system architecture: %w", err)
}
}
// Generate the instance metadata.
instanceMeta := make(map[string]string)
instanceMeta["name"] = d.name
instanceMeta["type"] = "virtual-machine"
instanceMeta["architecture"] = arch
if d.ephemeral {
instanceMeta["ephemeral"] = "true"
} else {
instanceMeta["ephemeral"] = "false"
}
// Go through the templates.
for tplPath, tpl := range metadata.Templates {
err = func(tplPath string, tpl *api.ImageMetadataTemplate) error {
var w *os.File
// Check if the template should be applied now.
found := slices.Contains(tpl.When, string(trigger))
if !found {
return nil
}
// Create the file itself.
w, err = os.Create(filepath.Join(path, fmt.Sprintf("%s.out", tpl.Template)))
if err != nil {
return err
}
// Fix ownership and mode.
err = w.Chmod(0o644)
if err != nil {
return err
}
defer logger.WarnOnError(w.Close, "Failed to close file")
// Read the template.
tplString, err := os.ReadFile(filepath.Join(d.TemplatesPath(), tpl.Template))
if err != nil {
return fmt.Errorf("Failed to read template file: %w", err)
}
configGet := func(confKey, confDefault *pongo2.Value) *pongo2.Value {
val, ok := d.expandedConfig[confKey.String()]
if !ok {
return confDefault
}
return pongo2.AsValue(strings.TrimRight(val, "\r\n"))
}
// Render the template.
err = internalUtil.RenderTemplateFile(w, string(tplString), pongo2.Context{
"trigger": trigger,
"path": tplPath,
"container": instanceMeta, // FIXME: remove once most images have moved away.
"instance": instanceMeta,
"config": d.expandedConfig,
"devices": d.expandedDevices,
"properties": tpl.Properties,
"config_get": configGet,
})
if err != nil {
return fmt.Errorf("Failed to render template: %w", err)
}
return w.Close()
}(tplPath, tpl)
if err != nil {
return err
}
}
return nil
}
// deviceBootPriorities returns a map keyed on device name containing the boot index to use.
// Qemu tries to boot devices in order of boot index (lowest first).
func (d *qemu) deviceBootPriorities(base int) (map[string]int, error) {
type devicePrios struct {
Name string
BootPrio uint32
}
devices := []devicePrios{}
for _, dev := range d.expandedDevices.Sorted() {
if dev.Config["type"] != "disk" && dev.Config["type"] != "nic" {
continue
}
bootPrio := uint32(0) // Default to lowest priority.
if dev.Config["boot.priority"] != "" {
prio, err := strconv.ParseInt(dev.Config["boot.priority"], 10, 32)
if err != nil {
return nil, fmt.Errorf("Invalid boot.priority for device %q: %w", dev.Name, err)
}
bootPrio = uint32(prio)
} else if dev.Config["path"] == "/" {
bootPrio = 1 // Set boot priority of root disk higher than any device without a boot prio.
}
devices = append(devices, devicePrios{Name: dev.Name, BootPrio: bootPrio})
}
// Sort devices by priority (use SliceStable so that devices with the same boot priority stay in the same
// order each boot based on the device order provided by the d.expandedDevices.Sorted() function).
// This is important because as well as providing a predictable boot index order, the boot index number can
// also be used for other properties (such as disk SCSI ID) which can result in it being given different
// device names inside the guest based on the device order.
sort.SliceStable(devices, func(i, j int) bool { return devices[i].BootPrio > devices[j].BootPrio })
sortedDevs := make(map[string]int, len(devices))
for bootIndex, dev := range devices {
sortedDevs[dev.Name] = bootIndex + base
}
return sortedDevs, nil
}
func (d *qemu) getStartupRTCAdjustment() time.Duration {
// Get the current values.
adjustment := d.parseRTC("volatile.vm.rtc_adjustment")
offset := d.parseRTC("volatile.vm.rtc_offset")
// Reset to handle new VM-generated updates.
adjustment += offset
offset = 0
changes := map[string]string{
"volatile.vm.rtc_adjustment": strconv.Itoa(adjustment),
"volatile.vm.rtc_offset": strconv.Itoa(offset),
}
err := d.VolatileSet(changes)
if err != nil {
d.logger.Error("Failed to set RTC change offset", logger.Ctx{"changes": changes, "err": err})
}
return time.Duration(adjustment) * time.Second
}
func (d *qemu) parseRTC(key string) int {
offset := 0
val, ok := d.localConfig[key]
if ok {
var err error
offset, err = strconv.Atoi(val)
if err != nil {
offset = 0
d.logger.Error("Failed to parse RTC volatile key")
}
}
return offset
}
// onRTCChange saves rtc change.
func (d *qemu) onRTCChange(change int) error {
offset := d.parseRTC("volatile.vm.rtc_offset")
if offset != change {
changes := map[string]string{"volatile.vm.rtc_offset": strconv.Itoa(change)}
err := d.VolatileSet(changes)
if err != nil {
d.logger.Error("Failed to set rtc change offset ", logger.Ctx{"changes": changes, "err": err})
}
return err
}
return nil
}
// gpuNativeContextConfig scans the device run configs for a virtio-gpu DRM native
// context GPU. It returns whether one is present, its resolved host DRM render node
// (which may be empty, meaning QEMU should use its default render node), and the
// host-visible blob window size in bytes for the device's hostmem property.
func (d *qemu) gpuNativeContextConfig(devConfs []*deviceConfig.RunConfig) (bool, string, string) {
enabled := false
rendernode := ""
hostmem := ""
for _, runConf := range devConfs {
isNativeContext := false
devRenderNode := ""
devHostmem := ""
for _, item := range runConf.GPUDevice {
switch item.Key {
case "gpuType":
if item.Value == "native-context" {
isNativeContext = true
}
case "rendernode":
devRenderNode = item.Value
case "hostmem":
devHostmem = item.Value
}
}
if isNativeContext {
enabled = true
if devRenderNode != "" {
rendernode = devRenderNode
}
if devHostmem != "" {
hostmem = devHostmem
}
}
}
return enabled, rendernode, hostmem
}
// generateQemuConfig generates the QEMU configuration.
func (d *qemu) generateQemuConfig(bs *qemuBootState, mountInfo *storagePools.MountInfo, busName string, vsockFD int, devConfs []*deviceConfig.RunConfig, fdFiles *[]*os.File) ([]monitorHook, error) {
var monHooks []monitorHook
isWindows := d.GuestOS() == osinfo.Windows
conf := qemuBase(&qemuBaseOpts{d.Architecture(), util.IsTrue(d.expandedConfig["security.iommu"]), bs.MachineType})
// Set OS Specific qemu args.
conf = append(conf, d.osVersionSpecificOptions()...)
err := d.addCPUMemoryConfig(&conf, bs)
if err != nil {
return nil, err
}
// Parse raw.qemu.
rawOptions := []string{}
if d.expandedConfig["raw.qemu"] != "" {
rawOptions, err = shellquote.Split(d.expandedConfig["raw.qemu"])
if err != nil {
return nil, err
}
}
// Allow disabling the UEFI firmware.
if slices.Contains(rawOptions, "-bios") || slices.Contains(rawOptions, "-kernel") {
d.logger.Warn("Starting VM without default firmware (-bios or -kernel in raw.qemu)")
} else if d.architectureSupportsUEFI(d.architecture) {
// Determine expected firmware.
firmwares, err := d.firmwarePairs()
if err != nil {
return nil, err
}
var efiCode string
unified := false
for _, firmware := range firmwares {
if firmware.Vars == "" {
// Unified firmware image (e.g. AMD SEV) with no separate vars store.
if util.PathExists(firmware.Code) {
efiCode = firmware.Code
unified = true
break
}
} else if util.PathExists(filepath.Join(d.Path(), filepath.Base(firmware.Vars))) {
efiCode = firmware.Code
break
}
}
if efiCode == "" {
return nil, fmt.Errorf("Unable to locate matching firmware: %+v", firmwares)
}
driveFirmwareOpts := qemuDriveFirmwareOpts{
roPath: efiCode,
}
if !unified {
// Open the UEFI NVRAM file and pass it via file descriptor to QEMU.
// This is so the QEMU process can still read/write the file after it has dropped its user privs.
nvRAMFile, err := os.Open(d.nvramPath())
if err != nil {
return nil, fmt.Errorf("Failed opening NVRAM file: %w", err)
}
driveFirmwareOpts.nvramPath = fmt.Sprintf("/dev/fd/%d", d.addFileDescriptor(fdFiles, nvRAMFile))
}
conf = append(conf, qemuDriveFirmware(&driveFirmwareOpts)...)
}
// QMP socket. Reference it through a short /proc/self/fd path to handle
// run paths that exceed the unix socket path limit.
monitorDir, err := os.OpenFile(d.RunPath(), unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return nil, err
}
monitorDirFD := d.addFileDescriptor(fdFiles, monitorDir)
monitorPath := fmt.Sprintf("/proc/self/fd/%d/qemu.monitor", monitorDirFD)
conf = append(conf, qemuControlSocket(&qemuControlSocketOpts{monitorPath})...)
// Console output.
conf = append(conf, qemuConsole()...)
// VM core info (memory dump).
if !util.IsTrue(d.expandedConfig["security.sev"]) && !slices.Contains([]int{osarch.ARCH_64BIT_POWERPC_LITTLE_ENDIAN, osarch.ARCH_64BIT_S390_BIG_ENDIAN}, d.architecture) {
conf = append(conf, qemuCoreInfo()...)
}
// Setup the bus allocator.
bus := qemuNewBus(busName, &conf)
// Add IOMMU.
if util.IsTrue(d.expandedConfig["security.iommu"]) && d.architectureSupportsUEFI(d.architecture) {
devBus, devAddr, multi := bus.allocateDirect()
iommuOpts := qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
}
conf = append(conf, qemuIOMMU(&iommuOpts, isWindows)...)
}
// Now add the fixed set of devices. The multi-function groups used for these fixed internal devices are
// specifically chosen to ensure that we consume exactly 4 PCI bus ports (on PCIe bus). This ensures that
// the first user device NIC added will use the 5th PCI bus port and will be consistently named enp5s0
// on PCIe (which we need to maintain compatibility with network configuration in our existing VM images).
// It's also meant to group all low-bandwidth internal devices onto a single address. PCIe bus allows a
// total of 256 devices, but this assumes 32 chassis * 8 function. By using VFs for the internal fixed
// devices we avoid consuming a chassis for each one.
devBus, devAddr, multi := bus.allocate(busFunctionGroupGeneric)
balloonOpts := qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
}
conf = append(conf, qemuBalloon(&balloonOpts)...)
devBus, devAddr, multi = bus.allocate(busFunctionGroupGeneric)
rngOpts := qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
}
conf = append(conf, qemuRNG(&rngOpts)...)
devBus, devAddr, multi = bus.allocate(busFunctionGroupGeneric)
keyboardOpts := qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
}
conf = append(conf, qemuKeyboard(&keyboardOpts)...)
devBus, devAddr, multi = bus.allocate(busFunctionGroupGeneric)
tabletOpts := qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
}
conf = append(conf, qemuTablet(&tabletOpts)...)
// Existing vsock ID from volatile.
vsockID, err := d.getVsockID()
if err != nil {
return nil, err
}
devBus, devAddr, multi = bus.allocate(busFunctionGroupGeneric)
vsockOpts := qemuVsockOpts{
dev: qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
},
vsockFD: vsockFD,
vsockID: vsockID,
}
conf = append(conf, qemuVsock(&vsockOpts)...)
info := DriverStatuses()[instancetype.VM].Info
_, spice := info.Features["spice"]
_, plan9 := info.Features["plan9"]
_, virtioSound := info.Features["virtio-sound"]
_, virtioVGA := info.Features["virtio-vga"]
devBus, devAddr, multi = bus.allocate(busFunctionGroupGeneric)
serialOpts := qemuSerialOpts{
dev: qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
},
charDevName: qemuSerialChardevName,
ringbufSizeBytes: qmp.RingbufSize,
spice: spice,
}
conf = append(conf, qemuSerial(&serialOpts)...)
// s390x doesn't really have USB.
if d.architecture != osarch.ARCH_64BIT_S390_BIG_ENDIAN {
devBus, devAddr, multi = bus.allocate(busFunctionGroupGeneric)
usbOpts := qemuUSBOpts{
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
ports: qemuSparseUSBPorts,
spice: spice,
}
conf = append(conf, qemuUSB(&usbOpts)...)
}
// virtio-sound-pci devices can't be migrated and don't have a CCW equivalent.
// NOTE: We can't use CanLiveMigrate during instance startup as the boot state hasn't yet been recorded.
if virtioSound && !isWindows && util.IsFalseOrEmpty(d.expandedConfig["migration.stateful"]) && d.architecture != osarch.ARCH_64BIT_S390_BIG_ENDIAN {
devBus, devAddr, multi = bus.allocate(busFunctionGroupGeneric)
audioOpts := qemuAudioOpts{
dev: qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
},
spice: spice,
}
conf = append(conf, qemuAudio(&audioOpts)...)
}
if util.IsTrue(d.expandedConfig["security.csm"]) {
// Allocate a regular entry to keep things aligned normally (avoid NICs getting a different name).
_, _, _ = bus.allocate(busFunctionGroupNone)
// Allocate a direct entry so the SCSI controller can be seen by seabios.
devBus, devAddr, multi = bus.allocateDirect()
} else {
devBus, devAddr, multi = bus.allocate(busFunctionGroupNone)
}
scsiOpts := qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
}
conf = append(conf, qemuSCSI(&scsiOpts, bs.getSCSIQueues())...)
// Export the config directory and agent as 9p drives when supported.
if !isWindows && plan9 {
devBus, devAddr, multi = bus.allocate(busFunctionGroup9p)
driveConfig9pOpts := qemuDriveConfigOpts{
dev: qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
},
name: "config",
protocol: "9p",
path: d.configDriveMountPath(),
}
conf = append(conf, qemuDriveConfig(&driveConfig9pOpts)...)
// Pass in the agents if INCUS_AGENT_PATH is set.
if util.PathExists(os.Getenv("INCUS_AGENT_PATH")) {
devBus, devAddr, multi = bus.allocate(busFunctionGroup9p)
driveConfig9pOpts := qemuDriveConfigOpts{
dev: qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
},
name: "agent",
protocol: "9p",
path: os.Getenv("INCUS_AGENT_PATH"),
}
conf = append(conf, qemuDriveConfig(&driveConfig9pOpts)...)
}
}
// If user has requested AMD SEV, check if supported and add to QEMU config.
if util.IsTrue(d.expandedConfig["security.sev"]) {
sevOpts, err := d.setupSEV(fdFiles)
if err != nil {
return nil, err
}
if sevOpts != nil {
for i := range conf {
if conf[i].Name == "machine" {
conf[i].Entries["memory-encryption"] = "sev0"
break
}
}
conf = append(conf, qemuSEV(sevOpts)...)
}
}
if util.IsTrue(d.expandedConfig["security.csm"]) {
// Allocate a regular entry to keep things aligned normally (avoid NICs getting a different name).
_, _, _ = bus.allocate(busFunctionGroupNone)
// Allocate a direct entry so the GPU can be seen by seabios.
devBus, devAddr, multi = bus.allocateDirect()
} else {
devBus, devAddr, multi = bus.allocate(busFunctionGroupNone)
}
// When any attached GPU device requests virtio-gpu DRM native context, the default
// emulated GPU is switched to a GL-capable device with blob and native context
// enabled rather than a plain virtio-gpu. This requires a recent QEMU.
nativeContext, _, hostmem := d.gpuNativeContextConfig(devConfs)
if nativeContext {
qemuVer, err := d.version()
if err != nil {
return nil, fmt.Errorf("GPU native context requires a known QEMU version: %w", err)
}
qemuVer11, _ := version.NewDottedVersion("11.0.0")
if qemuVer.Compare(qemuVer11) < 0 {
return nil, fmt.Errorf("GPU native context requires QEMU 11.0.0 or newer (have %s)", qemuVer.String())
}
}
gpuOpts := qemuGpuOpts{
dev: qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
},
architecture: d.Architecture(),
virtioVGA: virtioVGA,
nativeContext: nativeContext,
hostmem: hostmem,
}
conf = append(conf, qemuGPU(&gpuOpts)...)
// Dynamic devices.
base := 0
if slices.Contains(rawOptions, "-kernel") {
base = 1
}
bootIndexes, err := d.deviceBootPriorities(base)
if err != nil {
return nil, fmt.Errorf("Error calculating boot indexes: %w", err)
}
// Record the mounts we are going to do inside the VM using the agent.
agentMounts := []instancetype.VMAgentMount{}
// These devices are sorted so that NICs are added first to ensure that the first NIC can use the 5th
// PCIe bus port and will be consistently named enp5s0 for compatibility with network configuration in our
// existing VM images. Even on non-PCIe buses having NICs first means that their names won't change when
// other devices are added.
for _, runConf := range devConfs {
// Add drive devices.
if len(runConf.Mounts) > 0 {
for _, drive := range runConf.Mounts {
var monHook monitorHook
// Check if the user has overridden the bus.
busName := "virtio-scsi"
for _, opt := range drive.Opts {
if !strings.HasPrefix(opt, "bus=") {
continue
}
busName = strings.TrimPrefix(opt, "bus=")
break
}
qemuDev := make(map[string]any)
if slices.Contains([]string{"9p", "nvme", "virtio-blk", "virtiofs"}, busName) {
// Allocate a PCI(e) port and write it to the config file so QMP can "hotplug" the
// drive into it later.
functionGroup := busFunctionGroupNone
if busName == "9p" {
functionGroup = busFunctionGroup9p
}
devBus, devAddr, multi := bus.allocate(functionGroup)
// Populate the qemu device with port info.
qemuDev["bus"] = devBus
qemuDev["addr"] = devAddr
qemuDev["multifunction"] = multi
}
if drive.TargetPath == "/" {
monHook, err = d.addRootDriveConfig(qemuDev, mountInfo, bootIndexes, drive)
} else if drive.FSType == "9p" {
if busName == "9p" {
conf = append(conf, d.driveDirConfig9p(qemuDev, bus.name, &agentMounts, drive)...)
} else {
monHook, err = d.addDriveDirConfigVirtiofs(qemuDev, &agentMounts, drive)
}
} else {
monHook, err = d.addDriveConfig(qemuDev, bootIndexes, drive)
}
if err != nil {
return nil, fmt.Errorf("Failed setting up disk device %q: %w", drive.DevName, err)
}
if monHook != nil {
monHooks = append(monHooks, monHook)
}
}
}
// Add network device.
if len(runConf.NetworkInterface) > 0 {
qemuDev := make(map[string]any)
busName := bus.name
if runConf.UseUSBBus {
busName = "usb"
qemuDev["bus"] = "qemu_usb.0"
} else if slices.Contains([]string{"pcie", "pci"}, busName) {
// Allocate a PCI(e) port and write it to the config file so QMP can "hotplug" the
// NIC into it later.
devBus, devAddr, multi := bus.allocate(busFunctionGroupNone)
// Populate the qemu device with port info.
qemuDev["bus"] = devBus
qemuDev["addr"] = devAddr
if multi {
qemuDev["multifunction"] = true
}
}
monHook, err := d.addNetDevConfig(busName, qemuDev, bootIndexes, runConf.NetworkInterface)
if err != nil {
return nil, err
}
monHooks = append(monHooks, monHook)
}
// Add GPU device.
if len(runConf.GPUDevice) > 0 {
err = d.addGPUDevConfig(&conf, bus, runConf.GPUDevice)
if err != nil {
return nil, err
}
}
// Add PCI device.
if len(runConf.PCIDevice) > 0 {
err = d.addPCIDevConfig(&conf, bus, runConf.PCIDevice)
if err != nil {
return nil, err
}
}
// Add USB devices.
for _, usbDev := range runConf.USBDevice {
monHook, err := d.addUSBDeviceConfig(usbDev)
if err != nil {
return nil, err
}
monHooks = append(monHooks, monHook)
}
// Add TPM device.
if len(runConf.TPMDevice) > 0 {
err = d.addTPMDeviceConfig(&conf, runConf.TPMDevice, fdFiles)
if err != nil {
return nil, err
}
}
}
// VM generation ID is only available on x86.
if d.architecture == osarch.ARCH_64BIT_INTEL_X86 {
err = d.addVmgenDeviceConfig(&conf, d.localConfig["volatile.uuid.generation"])
if err != nil {
return nil, err
}
}
// Allocate 8 PCI slots for hotplug devices.
for range 8 {
bus.allocate(busFunctionGroupNone)
}
if !isWindows {
// Write the agent mount config.
agentMountJSON, err := json.Marshal(agentMounts)
if err != nil {
return nil, fmt.Errorf("Failed marshalling agent mounts to JSON: %w", err)
}
agentMountFile := filepath.Join(d.Path(), "config", "agent-mounts.json")
err = os.WriteFile(agentMountFile, agentMountJSON, 0o400)
if err != nil {
return nil, fmt.Errorf("Failed writing agent mounts file: %w", err)
}
}
// process any user-specified overrides
confOverride, ok := d.expandedConfig["raw.qemu.conf"]
if ok {
d.conf, err = qemuRawCfgOverride(conf, confOverride)
if err != nil {
return nil, err
}
} else {
d.conf = conf
}
return monHooks, nil
}
// writeQemuConfigFile writes the QEMU config file.
// It writes the config file inside the VM's log path.
func (d *qemu) writeQemuConfigFile(configPath string) error {
// Write the config file to disk.
sb := qemuStringifyCfg(d.conf...)
return os.WriteFile(configPath, []byte(sb.String()), 0o640)
}
// getCPUOpts retrieves configuration options for virtualized CPUs and memory.
func (d *qemu) getCPUOpts(cpuInfo *qemuCPUTopology, memSizeBytes int64) (*qemuCPUOpts, error) {
cpuOpts := qemuCPUOpts{
architecture: d.architecture,
}
hostNodes := []uint64{}
if cpuInfo.VCPUs == nil {
if cpuInfo.Explicit {
// An explicit CPU topology was requested, expose it verbatim to the guest.
// This is incompatible with CPU hotplugging.
cpuOpts.cpuSockets = cpuInfo.Sockets
cpuOpts.cpuCores = cpuInfo.Cores
cpuOpts.cpuThreads = cpuInfo.Threads
cpuOpts.cpuCount = cpuInfo.Sockets * cpuInfo.Cores * cpuInfo.Threads
} else if d.architectureSupportsCPUHotplug() {
// If not pinning, default to exposing cores.
// Only one CPU will be added here, as the others will be hotplugged during start.
cpuOpts.cpuCount = 1
cpuOpts.cpuCores = 1
cpuOpts.cpuSockets = 1
cpuOpts.cpuThreads = 1
// Expose the total requested by the user already so the hotplug limit can be set higher if needed.
cpuOpts.cpuRequested = cpuInfo.Cores
} else {
cpuOpts.cpuCount = cpuInfo.Cores
cpuOpts.cpuCores = cpuInfo.Cores
cpuOpts.cpuSockets = 1
cpuOpts.cpuThreads = 1
}
hostNodes = []uint64{0}
// Handle NUMA restrictions.
numaNodes := d.expandedConfig["limits.cpu.nodes"]
if numaNodes != "" {
if numaNodes == "balanced" {
numaNodes = d.expandedConfig["volatile.cpu.nodes"]
}
// Parse the NUMA restriction.
numaNodeSet, err := resources.ParseNumaNodeSet(numaNodes)
if err != nil {
return nil, err
}
cpuOpts.memoryHostNodes = numaNodeSet
}
} else {
// Figure out socket-id/core-id/thread-id for all vcpus.
vcpuSocket := map[uint64]uint64{}
vcpuCore := map[uint64]uint64{}
vcpuThread := map[uint64]uint64{}
vcpu := uint64(0)
for i := range cpuInfo.Sockets {
for j := range cpuInfo.Cores {
for k := range cpuInfo.Threads {
vcpuSocket[vcpu] = uint64(i)
vcpuCore[vcpu] = uint64(j)
vcpuThread[vcpu] = uint64(k)
vcpu++
}
}
}
// Prepare the NUMA map.
numa := []qemuNumaEntry{}
numaIDs := []uint64{}
numaNode := uint64(0)
for hostNode, entry := range cpuInfo.Nodes {
hostNodes = append(hostNodes, hostNode)
numaIDs = append(numaIDs, numaNode)
for _, vcpu := range entry {
numa = append(numa, qemuNumaEntry{
node: numaNode,
socket: vcpuSocket[vcpu],
core: vcpuCore[vcpu],
thread: vcpuThread[vcpu],
})
}
numaNode++
}
// Prepare context.
cpuOpts.cpuCount = len(cpuInfo.VCPUs)
cpuOpts.cpuSockets = cpuInfo.Sockets
cpuOpts.cpuCores = cpuInfo.Cores
cpuOpts.cpuThreads = cpuInfo.Threads
cpuOpts.cpuNumaNodes = numaIDs
cpuOpts.cpuNumaMapping = numa
cpuOpts.cpuNumaHostNodes = hostNodes
}
cpuOpts.hugepages = ""
if util.IsTrue(d.expandedConfig["limits.memory.hugepages"]) {
hugetlb, err := localUtil.HugepagesPath()
if err != nil {
return nil, err
}
cpuOpts.hugepages = hugetlb
}
// Determine per-node memory limit.
memSizeMB := memSizeBytes / 1024 / 1024
nodeMemory := memSizeMB
if d.architecture == osarch.ARCH_64BIT_INTEL_X86 {
nodeMemory = memSizeMB / int64(len(hostNodes))
}
cpuOpts.memory = nodeMemory
return &cpuOpts, nil
}
// addCPUMemoryConfig adds the qemu config required for setting the number of virtualised CPUs and memory.
// If sb is nil then no config is written.
func (d *qemu) addCPUMemoryConfig(conf *[]cfg.Section, bs *qemuBootState) error {
cpuOpts, err := d.getCPUOpts(bs.CPUTopology, bs.MemoryTopology.Base)
if err != nil {
return err
}
// A fixed topology is written verbatim, either due to CPU pinning or an explicit topology request.
cpuFixedTopology := bs.CPUTopology.VCPUs != nil || bs.CPUTopology.Explicit
*conf = append(*conf, qemuMemory(&qemuMemoryOpts{bs.MemoryTopology.Base / 1024 / 1024, bs.MemoryTopology.Max / 1024 / 1024})...)
*conf = append(*conf, qemuCPU(cpuOpts, cpuFixedTopology)...)
return nil
}
// addFileDescriptor adds a file path to the list of files to open and pass file descriptor to qemu.
// Returns the file descriptor number that qemu will receive.
func (d *qemu) addFileDescriptor(fdFiles *[]*os.File, file *os.File) int {
// Append the tap device file path to the list of files to be opened and passed to qemu.
*fdFiles = append(*fdFiles, file)
return 2 + len(*fdFiles) // Use 2+fdFiles count, as first user file descriptor is 3.
}
// ImageMetadataDir returns the metadata image directory for the given instance path.
func ImageMetadataDir(instancePath string) string {
return filepath.Join(instancePath, "image_metadata")
}
// imageMetadataDir returns the instance's metadata image directory.
func (d *qemu) imageMetadataDir() string {
return ImageMetadataDir(d.Path())
}
// imageMetadataPath returns the instance's image metadata file path.
func (d *qemu) imageMetadataPath(devName string) string {
return filepath.Join(d.imageMetadataDir(), fmt.Sprintf("%s.qcow2", devName))
}
// ensureMetadataImage creates the metadata file if needed.
func (d *qemu) ensureMetadataImage(rawPath string, devName string) (string, string, error) {
if rawPath == "" {
return "", "", errors.New("Raw disk path is empty")
}
isQcow2, err := d.isQCOW2(rawPath)
if err != nil {
return "", "", err
}
if isQcow2 {
// Image is qcow2 already, don't touch anything
return rawPath, "", nil
}
qcow2Dir := d.imageMetadataDir()
err = os.MkdirAll(qcow2Dir, 0o700)
if err != nil {
return "", "", fmt.Errorf("Failed creating metadata image directory: %w", err)
}
qcow2Path := d.imageMetadataPath(devName)
// If we already have metadata image, then just use it
if util.PathExists(qcow2Path) {
imgInfo, err := storageDrivers.Qcow2Info(qcow2Path)
if err != nil {
return "", "", err
}
if imgInfo.Format != storageDrivers.BlockVolumeTypeQcow2 {
return "", "", fmt.Errorf("Existing metadata image %q is not qcow2", qcow2Path)
}
// Get size of disk block device.
blockDiskSize, err := storageDrivers.BlockDiskSizeBytes(rawPath)
if err != nil {
return "", "", fmt.Errorf("Error getting block device size %q: %w", rawPath, err)
}
// Keep the metadata image in sync with the disk size as the volume
// may have been resized while the instance was stopped.
if int64(imgInfo.VirtualSize) != blockDiskSize {
err = d.resizeMetadataImage(qcow2Path, rawPath, blockDiskSize, int64(imgInfo.VirtualSize))
if err != nil {
return "", "", err
}
}
return qcow2Path, rawPath, nil
}
// Metadata image is not there yet, let's create it!
// To do this, we need a tiny trick.
// We can't use existing raw image (block device) with qemu-img, because
// qemu-img will overwrite it and we lose data. So, instead we should
// calculate it's size and create an empty temporary raw image with the same size
// and use it.
// Get size of disk block device.
blockDiskSize, err := storageDrivers.BlockDiskSizeBytes(rawPath)
if err != nil {
return "", "", fmt.Errorf("Error getting block device size %q: %w", rawPath, err)
}
rawSize := fmt.Sprintf("%d", blockDiskSize)
tmpPath := filepath.Join(qcow2Dir, fmt.Sprintf("%s.raw.tmp", devName))
defer func() {
_ = os.Remove(tmpPath)
}()
_, err = subprocess.RunCommand("qemu-img", "create", "-f", "raw", tmpPath, rawSize)
if err != nil {
return "", "", fmt.Errorf("Failed creating temporary raw data-file: %w", err)
}
_, err = subprocess.RunCommand("qemu-img", "create", "-f", "qcow2", "-o", fmt.Sprintf("data_file=%s,data_file_raw=on,preallocation=metadata", tmpPath), qcow2Path, rawSize)
if err != nil {
return "", "", fmt.Errorf("Failed creating qcow2 metadata image %q: %w", qcow2Path, err)
}
// Now everything is ready, we have a qcow2 image to keep metadata (bitmaps),
// and have set rawPath as a data-file for this image. But notice, it won't work
// just as it is, because QEMU will fail to open device from rawPath, instead we will
// have to use some trickery later to replace data-file in the existing image with
// /dev/fdset/<x> path and send rawPath as an FD.
return qcow2Path, rawPath, nil
}
// resizeMetadataImage resizes the qcow2 metadata image to match the raw disk size.
// The data-file has to be overridden as the one recorded in the image no longer exists.
func (d *qemu) resizeMetadataImage(qcow2Path string, rawPath string, newSize int64, oldSize int64) error {
fInfo, err := os.Stat(rawPath)
if err != nil {
return err
}
rawDriver := "file"
if linux.IsBlockdev(fInfo.Mode()) {
rawDriver = "host_device"
}
escape := func(s string) string {
return strings.ReplaceAll(s, ",", ",,")
}
args := []string{"resize"}
if newSize < oldSize {
args = append(args, "--shrink")
} else {
args = append(args, "--preallocation=metadata")
}
args = append(args, "--image-opts", fmt.Sprintf("driver=qcow2,file.filename=%s,data-file.driver=%s,data-file.filename=%s", escape(qcow2Path), rawDriver, escape(rawPath)), fmt.Sprintf("%d", newSize))
_, err = subprocess.RunCommand("qemu-img", args...)
if err != nil {
return fmt.Errorf("Failed resizing qcow2 metadata image %q: %w", qcow2Path, err)
}
return nil
}
// addRootDriveConfig adds the qemu config required for adding the root drive.
func (d *qemu) addRootDriveConfig(qemuDev map[string]any, mountInfo *storagePools.MountInfo, bootIndexes map[string]int, rootDriveConf deviceConfig.MountEntryItem) (monitorHook, error) {
if rootDriveConf.TargetPath != "/" {
return nil, errors.New("Non-root drive config supplied")
}
if !d.storagePool.Driver().Info().Remote && mountInfo.DiskPath == "" {
return nil, errors.New("No root disk path available from mount")
}
devPath, dataFilePath, err := d.ensureMetadataImage(mountInfo.DiskPath, rootDriveConf.DevName)
if err != nil {
return nil, err
}
// Generate a new device config with the root device path expanded.
driveConf := deviceConfig.MountEntryItem{
DevName: rootDriveConf.DevName,
DevPath: devPath,
DataFilePath: dataFilePath,
BackingPath: mountInfo.BackingPath,
Opts: rootDriveConf.Opts,
TargetPath: rootDriveConf.TargetPath,
Limits: rootDriveConf.Limits,
}
if d.storagePool.Driver().Info().Remote {
vol := d.storagePool.GetVolume(storageDrivers.VolumeTypeVM, storageDrivers.ContentTypeBlock, project.Instance(d.project.Name, d.name), nil)
if slices.Contains([]string{"ceph", "cephfs"}, d.storagePool.Driver().Info().Name) {
config := d.storagePool.ToAPI().Config
userName := config["ceph.user.name"]
if userName == "" {
userName = storageDrivers.CephDefaultUser
}
clusterName := config["ceph.cluster_name"]
if clusterName == "" {
clusterName = storageDrivers.CephDefaultUser
}
rbdImageName := storageDrivers.CephGetRBDImageName(vol, "", false)
driveConf.DevPath = device.DiskGetRBDFormat(clusterName, userName, config["ceph.osd.pool_name"], rbdImageName)
}
}
return d.addDriveConfig(qemuDev, bootIndexes, driveConf)
}
// driveDirConfig9p generates the qemu config required for adding a supplementary drive directory share using 9p.
func (d *qemu) driveDirConfig9p(qemuDev map[string]any, busName string, agentMounts *[]instancetype.VMAgentMount, driveConf deviceConfig.MountEntryItem) []cfg.Section {
mountTag := d.mountTagName(driveConf.DevName, qemuMountTag9pMaxLength)
agentMount := instancetype.VMAgentMount{
Source: mountTag,
Target: driveConf.TargetPath,
FSType: "9p",
// We need to specify to use the virtio transport to support more VM guest OSes.
// Also set the msize to 32MB to allow for reasonably fast 9p access.
Options: []string{"trans=virtio,msize=33554432"},
}
readonly := slices.Contains(driveConf.Opts, "ro")
// Indicate to agent to mount this readonly. Note: This is purely to indicate to VM guest that this is
// readonly, it should *not* be used as a security measure, as the VM guest could remount it R/W.
if readonly {
agentMount.Options = append(agentMount.Options, "ro")
}
// Record the mount for the agent.
*agentMounts = append(*agentMounts, agentMount)
// Add 9p share config.
driveDir9pOpts := qemuDriveDirOpts{
dev: qemuDevOpts{
busName: busName,
devBus: qemuDev["bus"].(string),
devAddr: qemuDev["addr"].(string),
multifunction: qemuDev["multifunction"].(bool),
},
devName: driveConf.DevName,
mountTag: mountTag,
readonly: readonly,
path: driveConf.DevPath,
protocol: "9p",
}
return qemuDriveDir(&driveDir9pOpts)
}
// addDriveDirConfigVirtiofs adds the qemu config required for adding a supplementary drive directory share using virtiofs.
func (d *qemu) addDriveDirConfigVirtiofs(qemuDev map[string]any, agentMounts *[]instancetype.VMAgentMount, driveConf deviceConfig.MountEntryItem) (monitorHook, error) {
escapedDeviceName := linux.PathNameEncode(driveConf.DevName)
deviceID := qemuDeviceIDPrefix + escapedDeviceName
mountTag := d.mountTagName(driveConf.DevName, qemuMountTagMaxLength)
if agentMounts != nil {
agentMount := instancetype.VMAgentMount{
Source: mountTag,
Target: driveConf.TargetPath,
FSType: "virtiofs",
}
// Indicate to agent to mount this readonly. Note: This is purely to indicate to VM guest that this is
// readonly, it should *not* be used as a security measure, as the VM guest could remount it R/W.
if slices.Contains(driveConf.Opts, "ro") {
agentMount.Options = append(agentMount.Options, "ro")
}
// Record the mount for the agent.
*agentMounts = append(*agentMounts, agentMount)
}
if qemuDev == nil {
qemuDev = map[string]any{}
}
qemuDev["driver"] = "vhost-user-fs-pci"
qemuDev["tag"] = mountTag
qemuDev["chardev"] = mountTag
qemuDev["id"] = deviceID
monHook := func(m *qmp.Monitor) error {
reverter := revert.New()
defer reverter.Fail()
// Detect virtiofsd path.
virtiofsdSockPath := filepath.Join(d.DevicesPath(), fmt.Sprintf("virtio-fs.%s.sock", linux.PathNameEncode(driveConf.DevName)))
if !util.PathExists(virtiofsdSockPath) {
return errors.New("Virtiofsd isn't running")
}
virtiofsSock, err := linux.DialUnix(virtiofsdSockPath)
if err != nil {
return fmt.Errorf("Error connecting to virtiofs socket %q: %w", virtiofsdSockPath, err)
}
defer logger.WarnOnError(virtiofsSock.Close, "Failed to close virtiofs socket") // Close file after device has been added.
virtiofsFile, err := virtiofsSock.File()
if err != nil {
return fmt.Errorf("Error opening virtiofs socket %q: %w", virtiofsdSockPath, err)
}
err = m.SendFile(virtiofsdSockPath, virtiofsFile)
if err != nil {
return fmt.Errorf("Failed to send virtiofs file descriptor: %w", err)
}
reverter.Add(func() { _ = m.CloseFile(virtiofsdSockPath) })
err = m.AddCharDevice(map[string]any{
"id": mountTag,
"backend": map[string]any{
"type": "socket",
"data": map[string]any{
"addr": map[string]any{
"type": "fd",
"data": map[string]any{
"str": virtiofsdSockPath,
},
},
"server": false,
},
},
})
if err != nil {
return fmt.Errorf("Failed to add the character device: %w", err)
}
reverter.Add(func() { _ = m.RemoveCharDevice(mountTag) })
_, ok := qemuDev["bus"]
if !ok {
// Try to get a PCI address for hotplugging.
pciDeviceName, err := d.getPCIHotplug()
if err != nil {
return err
}
d.logger.Debug("Using PCI bus device to hotplug virtiofs into", logger.Ctx{"device": driveConf.DevName, "port": pciDeviceName, "was": qemuDev["bus"]})
qemuDev["bus"] = pciDeviceName
qemuDev["addr"] = "00.0"
}
err = m.AddDevice(qemuDev)
if err != nil {
return fmt.Errorf("Failed to add the virtiofs device: %w", err)
}
reverter.Success()
return nil
}
return monHook, nil
}
// addDriveConfig adds the qemu config required for adding a supplementary drive.
func (d *qemu) addDriveConfig(qemuDev map[string]any, bootIndexes map[string]int, driveConf deviceConfig.MountEntryItem) (monitorHook, error) {
aioMode := "native" // Use native kernel async IO and O_DIRECT by default.
cacheMode := "none" // Bypass host cache, use O_DIRECT semantics by default.
media := "disk"
isRBDImage := strings.HasPrefix(driveConf.DevPath, device.RBDFormatPrefix)
// Use io_uring over native for added performance when supported by QEMU.
info := DriverStatuses()[instancetype.VM].Info
_, ioUring := info.Features["io_uring"]
if slices.Contains(driveConf.Opts, device.DiskIOUring) && ioUring {
aioMode = "io_uring"
}
var isBlockDev bool
var srcDevPath string
// Detect device caches and I/O modes.
if isRBDImage {
// For RBD, we want writeback to allow for the system-configured "rbd cache" to take effect if present.
cacheMode = "writeback"
} else {
srcDevPath = driveConf.DevPath // This should not be used for passing to QEMU, only for probing.
// Detect if existing file descriptor format is being supplied.
if strings.HasPrefix(driveConf.DevPath, fmt.Sprintf("%s:", device.DiskFileDescriptorMountPrefix)) {
// Expect devPath in format "fd:<fdNum>:<devPath>".
devPathParts := strings.SplitN(driveConf.DevPath, ":", 3)
if len(devPathParts) != 3 || !strings.HasPrefix(driveConf.DevPath, fmt.Sprintf("%s:", device.DiskFileDescriptorMountPrefix)) {
return nil, fmt.Errorf("Unexpected devPath file descriptor format %q", driveConf.DevPath)
}
// Map the file descriptor to the file descriptor path it will be in the QEMU process.
fd, err := strconv.Atoi(devPathParts[1])
if err != nil {
return nil, fmt.Errorf("Invalid file descriptor %q: %w", devPathParts[1], err)
}
// Extract original dev path for additional probing below.
srcDevPath = devPathParts[2]
if srcDevPath == "" {
return nil, errors.New("Device source path is empty")
}
driveConf.DevPath = fmt.Sprintf("/proc/self/fd/%d", fd)
} else if driveConf.TargetPath != "/" {
// Only the root disk device is allowed to pass local devices to us without using an FD.
return nil, fmt.Errorf("Invalid device path format %q", driveConf.DevPath)
}
srcDevPathInfo, err := os.Stat(srcDevPath)
if err != nil {
return nil, fmt.Errorf("Invalid source path %q: %w", srcDevPath, err)
}
isBlockDev = linux.IsBlockdev(srcDevPathInfo.Mode())
// Handle I/O mode configuration.
if !isBlockDev {
// Disk dev path is a file, check what the backing filesystem is.
fsType, err := linux.DetectFilesystem(srcDevPath)
if err != nil {
return nil, fmt.Errorf("Failed detecting filesystem type of %q: %w", srcDevPath, err)
}
// If backing FS is ZFS or BTRFS, avoid using direct I/O and use host page cache only.
// We've seen ZFS lock up and BTRFS checksum issues when using direct I/O on image files.
if fsType == "zfs" || fsType == "btrfs" {
aioMode = "threads"
cacheMode = "writeback" // Use host cache, with neither O_DSYNC nor O_DIRECT semantics.
} else {
// Use host cache, with neither O_DSYNC nor O_DIRECT semantics if filesystem
// doesn't support Direct I/O.
f, err := os.OpenFile(srcDevPath, unix.O_DIRECT|unix.O_RDONLY, 0)
if err != nil {
cacheMode = "writeback"
} else {
_ = f.Close() // Don't leak FD.
}
}
if cacheMode == "writeback" && driveConf.FSType != "iso9660" {
// Only warn about using writeback cache if the drive image is writable.
d.logger.Warn("Using writeback cache I/O", logger.Ctx{"device": driveConf.DevName, "devPath": srcDevPath, "fsType": fsType})
}
} else if !slices.Contains(driveConf.Opts, device.DiskDirectIO) {
// If drive config indicates we need to use unsafe I/O then use it.
d.logger.Warn("Using unsafe cache I/O", logger.Ctx{"device": driveConf.DevName, "devPath": srcDevPath})
aioMode = "threads"
cacheMode = "unsafe" // Use host cache, but ignore all sync requests from guest.
}
}
// Special case ISO images as cdroms.
if driveConf.FSType == "iso9660" {
media = "cdrom"
}
// Check if the user has overridden the bus.
bus := "virtio-scsi"
for _, opt := range driveConf.Opts {
if !strings.HasPrefix(opt, "bus=") {
continue
}
bus = strings.TrimPrefix(opt, "bus=")
break
}
// Check if the user has overridden the cache mode.
for _, opt := range driveConf.Opts {
if !strings.HasPrefix(opt, "cache=") {
continue
}
cacheMode = strings.TrimPrefix(opt, "cache=")
break
}
// Check if the user has overridden the WWN.
var wwn string
for _, opt := range driveConf.Opts {
if !strings.HasPrefix(opt, "wwn=") {
continue
}
wwn = strings.TrimPrefix(opt, "wwn=")
break
}
// QMP uses two separate values for the cache.
directCache := true // Bypass host cache, use O_DIRECT semantics by default.
noFlushCache := false // Don't ignore any flush requests for the device.
switch cacheMode {
case "unsafe":
aioMode = "threads"
directCache = false
noFlushCache = true
case "writeback":
aioMode = "threads"
directCache = false
}
escapedDeviceName := linux.PathNameEncode(driveConf.DevName)
blockDev := map[string]any{
"aio": aioMode,
"cache": map[string]any{
"direct": directCache,
"no-flush": noFlushCache,
},
"discard": "unmap", // Forward as an unmap request. This is the same as `discard=on` in the qemu config file.
"driver": "file",
"node-name": d.blockNodeName(escapedDeviceName),
"read-only": false,
}
var rbdSecret string
// If driver is "file", QEMU requires the file to be a regular file.
// However, if the file is a character or block device, driver needs to be set to "host_device".
if isBlockDev {
blockDev["driver"] = "host_device"
} else if isRBDImage {
blockDev["driver"] = "rbd"
poolName, imageName, opts, err := device.DiskParseRBDFormat(driveConf.DevPath)
if err != nil {
return nil, fmt.Errorf("Failed parsing rbd string: %w", err)
}
// Scan & pass through options.
clusterName := storageDrivers.CephDefaultCluster
userName := storageDrivers.CephDefaultUser
blockDev["pool"] = poolName
blockDev["image"] = imageName
for key, val := range opts {
// We use 'id' where qemu uses 'user'.
switch key {
case "id":
blockDev["user"] = val
userName = val
case "cluster":
clusterName = val
default:
blockDev[key] = val
}
}
// Parse the secret (QEMU runs unprivileged and can't read the keyring directly).
rbdSecret, err = storageDrivers.CephKeyring(clusterName, userName)
if err != nil {
return nil, err
}
// The aio option isn't available when using the rbd driver.
delete(blockDev, "aio")
}
readonly := slices.Contains(driveConf.Opts, "ro") || media == "cdrom"
if readonly {
blockDev["read-only"] = true
}
if !isRBDImage {
blockDev["locking"] = "off"
}
if qemuDev == nil {
qemuDev = map[string]any{}
}
qemuDev["id"] = fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName)
nodeName, ok := blockDev["node-name"].(string)
if !ok {
return nil, fmt.Errorf("Invalid block device node-name for %q", driveConf.DevName)
}
qemuDev["drive"] = nodeName
// Max serial length is 36 characters: prefix + 30 chars.
// For nvme and virtio-blk, the maximum serial length is 20 characters: prefix + 14 chars.
serialMaxLength := 30
if slices.Contains([]string{"nvme", "virtio-blk"}, bus) {
serialMaxLength = 14
}
qemuDev["serial"] = fmt.Sprintf("%s%s", qemuBlockDevIDPrefix, hashValue(escapedDeviceName, serialMaxLength))
if wwn != "" {
wwnID, err := strconv.ParseUint(strings.TrimPrefix(wwn, "0x"), 16, 64)
if err != nil {
return nil, err
}
qemuDev["wwn"] = wwnID
}
if bus == "virtio-scsi" {
qemuDev["device_id"] = d.blockNodeName(escapedDeviceName)
qemuDev["channel"] = 0
qemuDev["lun"] = 1
qemuDev["bus"] = "qemu_scsi.0"
switch media {
case "disk":
qemuDev["driver"] = "scsi-hd"
case "cdrom":
qemuDev["driver"] = "scsi-cd"
}
} else if slices.Contains([]string{"nvme", "virtio-blk"}, bus) {
if qemuDev["bus"] == nil {
// Try to get a PCI address for hotplugging.
pciDeviceName, err := d.getPCIHotplug()
if err != nil {
return nil, err
}
d.logger.Debug("Using PCI bus device to hotplug drive into", logger.Ctx{"device": driveConf.DevName, "port": pciDeviceName})
qemuDev["bus"] = pciDeviceName
qemuDev["addr"] = "00.0"
}
qemuDev["driver"] = bus
} else if bus == "usb" {
qemuDev["lun"] = 0
switch media {
case "disk":
qemuDev["driver"] = "scsi-hd"
case "cdrom":
qemuDev["driver"] = "scsi-cd"
}
}
if bootIndexes != nil {
qemuDev["bootindex"] = bootIndexes[driveConf.DevName]
}
monHook := func(m *qmp.Monitor) error {
reverter := revert.New()
defer reverter.Fail()
nodeName := d.blockNodeName(escapedDeviceName)
if isRBDImage {
secretID := fmt.Sprintf("pool_%s_%s", blockDev["pool"], blockDev["user"])
err := m.AddSecret(secretID, rbdSecret)
if err != nil {
return err
}
blockDev["key-secret"] = secretID
} else {
permissions := unix.O_RDWR
if readonly {
permissions = unix.O_RDONLY
}
if directCache {
permissions |= unix.O_DIRECT
}
f, err := os.OpenFile(driveConf.DevPath, permissions, 0)
if err != nil {
return fmt.Errorf("Failed opening file descriptor for disk device %q: %w", driveConf.DevName, err)
}
defer logger.WarnOnError(f.Close, "Failed to close file")
info, err := m.SendFileWithFDSet(nodeName, f, readonly)
if err != nil {
return fmt.Errorf("Failed sending file descriptor of %q for disk device %q: %w", f.Name(), driveConf.DevName, err)
}
reverter.Add(func() {
_ = m.RemoveFDFromFDSet(nodeName)
})
isQcow2, err := d.isQCOW2(srcDevPath)
if err != nil {
return fmt.Errorf("Failed checking disk format: %w", err)
}
if isQcow2 {
srcDevPathInfo, err := os.Stat(srcDevPath)
if err != nil {
return fmt.Errorf("Invalid source path %q: %w", srcDevPath, err)
}
isBlockDev := linux.IsBlockdev(srcDevPathInfo.Mode())
blockDevFile := map[string]any{
"filename": fmt.Sprintf("/dev/fdset/%d", info.ID),
"aio": aioMode,
"cache": map[string]any{
"direct": directCache,
"no-flush": noFlushCache,
},
}
if isBlockDev {
blockDevFile["driver"] = "host_device"
} else {
blockDevFile["driver"] = "file"
}
blockDev = map[string]any{
"driver": "qcow2",
"discard": "unmap", // Forward as an unmap request. This is the same as `discard=on` in the qemu config file.
"node-name": d.blockNodeName(escapedDeviceName),
"read-only": false,
"file": blockDevFile,
}
// If there are any children, load block information about them.
if len(driveConf.BackingPath) > 0 {
backingBlockDev, err := d.qcow2BlockDev(m, nodeName, aioMode, directCache, noFlushCache, permissions, readonly, driveConf.BackingPath, 0)
if err != nil {
return err
}
blockDev["backing"] = backingBlockDev
}
// If the qcow2 has data-file set, add that file to the FD set and
// configure the qcow2 blockdev to use /dev/fdset/<x> path.
if driveConf.DataFilePath != "" {
dataDev, err := buildDataFileInfo(nodeName, m, driveConf, permissions, readonly, aioMode, directCache, noFlushCache)
if err != nil {
return err
}
// Tell QEMU to ignore qcow2's data-file path and use the one we provide
blockDev["data-file"] = dataDev
}
} else {
blockDev["filename"] = fmt.Sprintf("/dev/fdset/%d", info.ID)
}
}
err := m.AddBlockDevice(blockDev, qemuDev, bus == "usb")
if err != nil {
return fmt.Errorf("Failed adding block device for disk device %q: %w", driveConf.DevName, err)
}
if driveConf.Limits != nil {
err = m.SetBlockThrottle(qemuDev["id"].(string), int(driveConf.Limits.ReadBytes), int(driveConf.Limits.WriteBytes), int(driveConf.Limits.ReadIOps), int(driveConf.Limits.WriteIOps))
if err != nil {
return fmt.Errorf("Failed applying limits for disk device %q: %w", driveConf.DevName, err)
}
}
reverter.Success()
return nil
}
return monHook, nil
}
// addNetDevConfig adds the qemu config required for adding a network device.
// The qemuDev map is expected to be preconfigured with the settings for an existing port to use for the device.
func (d *qemu) addNetDevConfig(busName string, qemuDev map[string]any, bootIndexes map[string]int, nicConfig []deviceConfig.RunConfigItem) (monitorHook, error) {
reverter := revert.New()
defer reverter.Fail()
var devName, nicName, devHwaddr, pciSlotName, pciIOMMUGroup, vDPADevName, vhostVDPAPath, maxVQP string
connected := true
for _, nicItem := range nicConfig {
switch nicItem.Key {
case "devName":
devName = nicItem.Value
case "link":
nicName = nicItem.Value
case "hwaddr":
devHwaddr = nicItem.Value
case "pciSlotName":
pciSlotName = nicItem.Value
case "pciIOMMUGroup":
pciIOMMUGroup = nicItem.Value
case "vDPADevName":
vDPADevName = nicItem.Value
case "vhostVDPAPath":
vhostVDPAPath = nicItem.Value
case "maxVQP":
maxVQP = nicItem.Value
case "connected":
connected = util.IsTrueOrEmpty(nicItem.Value)
}
}
escapedDeviceName := linux.PathNameEncode(devName)
qemuDev["id"] = fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName)
if len(bootIndexes) > 0 {
bootIndex, found := bootIndexes[devName]
if found {
qemuDev["bootindex"] = bootIndex
}
}
var monHook func(m *qmp.Monitor) error
// configureQueues modifies qemuDev with the queue configuration based on vCPUs.
// Returns the number of queues to use with NIC.
configureQueues := func(cpuCount int) int {
// Number of queues is the same as number of vCPUs. Run with a minimum of two queues.
queueCount := max(cpuCount, 2)
// Number of vectors is number of vCPUs * 2 (RX/TX) + 2 (config/control MSI-X).
vectors := 2*queueCount + 2
if busName != "usb" {
qemuDev["mq"] = true
if slices.Contains([]string{"pcie", "pci"}, busName) {
qemuDev["vectors"] = vectors
}
}
return queueCount
}
// tapMonHook is a helper function used as the monitor hook for macvtap and tap interfaces to open
// multi-queue file handles to both the interface device and the vhost-net device and pass them to QEMU.
tapMonHook := func(deviceFile func() (*os.File, error)) func(m *qmp.Monitor) error {
return func(m *qmp.Monitor) error {
reverter := revert.New()
defer reverter.Fail()
cpus, err := m.QueryCPUs()
if err != nil {
return errors.New("Failed getting CPU list for NIC queues")
}
queueCount := configureQueues(len(cpus))
// Enable vhost_net offloading if available.
info := DriverStatuses()[instancetype.VM].Info
_, vhostNetEnabled := info.Features["vhost_net"]
// Open the device once for each queue and pass to QEMU.
fds := make([]string, 0, queueCount)
vhostfds := make([]string, 0, queueCount)
// Collect the opened file handles so they can all be closed once the devices have been added.
openFiles := make([]*os.File, 0, queueCount)
defer func() {
for _, f := range openFiles {
_ = f.Close()
}
}()
for i := range queueCount {
devFile, err := deviceFile()
if err != nil {
return fmt.Errorf("Error opening netdev file for queue %d: %w", i, err)
}
openFiles = append(openFiles, devFile)
devFDName := fmt.Sprintf("%s.%d", devFile.Name(), i)
err = m.SendFile(devFDName, devFile)
if err != nil {
return fmt.Errorf("Failed to send %q file descriptor for queue %d: %w", devFDName, i, err)
}
reverter.Add(func() { _ = m.CloseFile(devFDName) })
fds = append(fds, devFDName)
if vhostNetEnabled {
// Open a vhost-net file handle for each device file handle.
vhostFile, err := os.OpenFile("/dev/vhost-net", os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("Error opening /dev/vhost-net for queue %d: %w", i, err)
}
openFiles = append(openFiles, vhostFile)
vhostFDName := fmt.Sprintf("%s.%d", vhostFile.Name(), i)
err = m.SendFile(vhostFDName, vhostFile)
if err != nil {
return fmt.Errorf("Failed to send %q file descriptor for queue %d: %w", vhostFDName, i, err)
}
reverter.Add(func() { _ = m.CloseFile(vhostFDName) })
vhostfds = append(vhostfds, vhostFDName)
}
}
qemuNetDev := map[string]any{
"id": fmt.Sprintf("%s%s", qemuNetDevIDPrefix, escapedDeviceName),
"type": "tap",
"vhost": vhostNetEnabled,
}
if slices.Contains([]string{"pcie", "pci"}, busName) {
qemuDev["driver"] = "virtio-net-pci"
} else if busName == "ccw" {
qemuDev["driver"] = "virtio-net-ccw"
} else if busName == "usb" {
qemuDev["driver"] = "usb-net"
}
qemuNetDev["fds"] = strings.Join(fds, ":")
if len(vhostfds) > 0 {
qemuNetDev["vhostfds"] = strings.Join(vhostfds, ":")
}
netDevID, ok := qemuNetDev["id"].(string)
if !ok {
return errors.New("Invalid network device ID")
}
qemuDev["netdev"] = netDevID
qemuDev["mac"] = devHwaddr
err = m.AddNIC(qemuNetDev, qemuDev, connected)
if err != nil {
return fmt.Errorf("Failed setting up device %q: %w", devName, err)
}
reverter.Success()
return nil
}
}
// Detect MACVTAP interface types and figure out which tap device is being used.
// This is so we can open a file handle to the tap device and pass it to the qemu process.
if util.PathExists(fmt.Sprintf("/sys/class/net/%s/macvtap", nicName)) {
content, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/ifindex", nicName))
if err != nil {
return nil, fmt.Errorf("Error getting tap device ifindex: %w", err)
}
ifindex, err := strconv.Atoi(strings.TrimSpace(string(content)))
if err != nil {
return nil, fmt.Errorf("Error parsing tap device ifindex: %w", err)
}
devFile := func() (*os.File, error) {
return os.OpenFile(fmt.Sprintf("/dev/tap%d", ifindex), os.O_RDWR, 0)
}
monHook = tapMonHook(devFile)
} else if util.PathExists(fmt.Sprintf("/sys/class/net/%s/tun_flags", nicName)) {
// Detect TAP interface and use IOCTL TUNSETIFF on /dev/net/tun to get the file handle to it.
// This is so we can open a file handle to the tap device and pass it to the qemu process.
devFile := func() (*os.File, error) {
reverter := revert.New()
defer reverter.Fail()
f, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0)
if err != nil {
return nil, err
}
reverter.Add(func() { _ = f.Close() })
ifr, err := unix.NewIfreq(nicName)
if err != nil {
return nil, fmt.Errorf("Error creating new ifreq for %q: %w", nicName, err)
}
// These settings need to be compatible with what the device created the interface with
// and what QEMU is expecting.
ifr.SetUint16(unix.IFF_TAP | unix.IFF_NO_PI | unix.IFF_ONE_QUEUE | unix.IFF_MULTI_QUEUE | unix.IFF_VNET_HDR)
// Sets the file handle to point to the requested NIC interface.
err = unix.IoctlIfreq(int(f.Fd()), unix.TUNSETIFF, ifr)
if err != nil {
return nil, fmt.Errorf("Error getting TAP file handle for %q: %w", nicName, err)
}
reverter.Success()
return f, nil
}
monHook = tapMonHook(devFile)
} else if util.PathExists(vhostVDPAPath) {
monHook = func(m *qmp.Monitor) error {
reverter := revert.New()
defer reverter.Fail()
vdpaDevFile, err := os.OpenFile(vhostVDPAPath, os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("Error opening vDPA device file %q: %w", vdpaDevFile.Name(), err)
}
defer logger.WarnOnError(vdpaDevFile.Close, "Failed to close vDPA device file") // Close file after device has been added.
vDPADevFDName := fmt.Sprintf("%s.0", vdpaDevFile.Name())
err = m.SendFile(vDPADevFDName, vdpaDevFile)
if err != nil {
return fmt.Errorf("Failed to send %q file descriptor: %w", vDPADevFDName, err)
}
reverter.Add(func() { _ = m.CloseFile(vDPADevFDName) })
queues, err := strconv.Atoi(maxVQP)
if err != nil {
return fmt.Errorf("Failed to convert maxVQP (%q) to int: %w", maxVQP, err)
}
qemuNetDev := map[string]any{
"id": fmt.Sprintf("vhost-%s", vDPADevName),
"type": "vhost-vdpa",
"vhostfd": vDPADevFDName,
"queues": queues,
}
if slices.Contains([]string{"pcie", "pci"}, busName) {
qemuDev["driver"] = "virtio-net-pci"
} else if busName == "ccw" {
qemuDev["driver"] = "virtio-net-ccw"
} else if busName == "usb" {
qemuDev["driver"] = "usb-net"
}
netDevID, ok := qemuNetDev["id"].(string)
if !ok {
return errors.New("Invalid network device ID")
}
qemuDev["netdev"] = netDevID
qemuDev["page-per-vq"] = true
qemuDev["iommu_platform"] = true
qemuDev["disable-legacy"] = true
err = m.AddNIC(qemuNetDev, qemuDev, connected)
if err != nil {
return fmt.Errorf("Failed setting up device %q: %w", devName, err)
}
reverter.Success()
return nil
}
} else if pciSlotName != "" {
// Detect physical passthrough device.
if slices.Contains([]string{"pcie", "pci"}, busName) {
qemuDev["driver"] = "vfio-pci"
} else if busName == "ccw" {
qemuDev["driver"] = "vfio-ccw"
}
qemuDev["host"] = pciSlotName
if d.state.OS.UnprivUser != "" {
if pciIOMMUGroup == "" {
return nil, errors.New("No PCI IOMMU group supplied")
}
vfioGroupFile := fmt.Sprintf("/dev/vfio/%s", pciIOMMUGroup)
err := os.Chown(vfioGroupFile, int(d.state.OS.UnprivUID), -1)
if err != nil {
return nil, fmt.Errorf("Failed to chown vfio group device %q: %w", vfioGroupFile, err)
}
reverter.Add(func() { _ = os.Chown(vfioGroupFile, 0, -1) })
}
monHook = func(m *qmp.Monitor) error {
err := m.AddNIC(nil, qemuDev, connected)
if err != nil {
return fmt.Errorf("Failed setting up device %q: %w", devName, err)
}
return nil
}
}
if monHook == nil {
return nil, errors.New("Unrecognised device type")
}
reverter.Success()
return monHook, nil
}
// writeNICDevConfig writes the NIC config for the specified device into the NICConfigDir.
// This will be used by the agent to rename the NIC interfaces inside the VM guest.
func (d *qemu) writeNICDevConfig(mtuStr string, devName string, nicName string, devHwaddr string) error {
// Parse MAC address to ensure it is in a canonical form (avoiding casing/presentation differences).
hw, err := net.ParseMAC(devHwaddr)
if err != nil {
return fmt.Errorf("Failed parsing MAC %q: %w", devHwaddr, err)
}
nicConfig := deviceConfig.NICConfig{
DeviceName: devName,
NICName: nicName,
MACAddress: hw.String(),
}
if mtuStr != "" {
mtuInt, err := strconv.ParseUint(mtuStr, 10, 32)
if err != nil {
return fmt.Errorf("Failed parsing MTU: %w", err)
}
nicConfig.MTU = uint32(mtuInt)
}
nicConfigBytes, err := json.Marshal(nicConfig)
if err != nil {
return fmt.Errorf("Failed encoding NIC config: %w", err)
}
nicFile := filepath.Join(d.Path(), "config", deviceConfig.NICConfigDir, fmt.Sprintf("%s.json", linux.PathNameEncode(nicConfig.DeviceName)))
err = os.WriteFile(nicFile, nicConfigBytes, 0o700)
if err != nil {
return fmt.Errorf("Failed writing NIC config: %w", err)
}
return nil
}
// addPCIDevConfig adds the qemu config required for adding a raw PCI device.
func (d *qemu) addPCIDevConfig(conf *[]cfg.Section, bus *qemuBus, pciConfig []deviceConfig.RunConfigItem) error {
var devName, pciSlotName string
firmware := true
for _, pciItem := range pciConfig {
switch pciItem.Key {
case "devName":
devName = pciItem.Value
case "pciSlotName":
pciSlotName = pciItem.Value
case "firmware":
firmware = util.IsTrueOrEmpty(pciItem.Value)
}
}
devBus, devAddr, multi := bus.allocate(fmt.Sprintf("incus_%s", devName))
pciPhysicalOpts := qemuPCIPhysicalOpts{
dev: qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
},
devName: devName,
pciSlotName: pciSlotName,
firmware: firmware,
}
*conf = append(*conf, qemuPCIPhysical(&pciPhysicalOpts)...)
return nil
}
// addGPUDevConfig adds the qemu config required for adding a GPU device.
func (d *qemu) addGPUDevConfig(conf *[]cfg.Section, bus *qemuBus, gpuConfig []deviceConfig.RunConfigItem) error {
var devName, pciSlotName, vgpu, gpuType string
for _, gpuItem := range gpuConfig {
switch gpuItem.Key {
case "devName":
devName = gpuItem.Value
case "pciSlotName":
pciSlotName = gpuItem.Value
case "vgpu":
vgpu = gpuItem.Value
case "gpuType":
gpuType = gpuItem.Value
}
}
// A native-context GPU is not passed through as a PCI device. It is realized by the
// default virtio-gpu device (configured for GL/blob/native-context in generateQemuConfig)
// together with QEMU's egl-headless display, so there is nothing to add here.
if gpuType == "native-context" {
return nil
}
vgaMode := func() bool {
// No VGA mode on mdev.
if vgpu != "" {
return false
}
// No VGA mode on non-x86.
if d.architecture != osarch.ARCH_64BIT_INTEL_X86 {
return false
}
// Only enable if present on the card.
if !util.PathExists(filepath.Join("/sys/bus/pci/devices", pciSlotName, "boot_vga")) {
return false
}
// Skip SRIOV VFs as those are shared with the host card.
if util.PathExists(filepath.Join("/sys/bus/pci/devices", pciSlotName, "physfn")) {
return false
}
return true
}()
devBus, devAddr, multi := bus.allocate(fmt.Sprintf("incus_%s", devName))
gpuDevPhysicalOpts := qemuGPUDevPhysicalOpts{
dev: qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
},
devName: devName,
pciSlotName: pciSlotName,
vga: vgaMode,
vgpu: vgpu,
}
// Add main GPU device in VGA mode to qemu config.
*conf = append(*conf, qemuGPUDevPhysical(&gpuDevPhysicalOpts)...)
var iommuGroupPath string
if vgpu != "" {
iommuGroupPath = filepath.Join("/sys/bus/mdev/devices", vgpu, "iommu_group", "devices")
} else {
// Add any other related IOMMU VFs as generic PCI devices.
iommuGroupPath = filepath.Join("/sys/bus/pci/devices", pciSlotName, "iommu_group", "devices")
}
if util.PathExists(iommuGroupPath) {
// Extract parent slot name by removing any virtual function ID.
parts := strings.SplitN(pciSlotName, ".", 2)
prefix := parts[0]
// Iterate the members of the IOMMU group and override any that match the parent slot name prefix.
err := filepath.Walk(iommuGroupPath, func(path string, _ os.FileInfo, err error) error {
if err != nil {
return err
}
iommuSlotName := filepath.Base(path) // Virtual function's address is dir name.
// Match any VFs that are related to the GPU device (but not the GPU device itself).
if strings.HasPrefix(iommuSlotName, prefix) && iommuSlotName != pciSlotName {
// Add VF device without VGA mode to qemu config.
devBus, devAddr, multi := bus.allocate(fmt.Sprintf("incus_%s", devName))
gpuDevPhysicalOpts := qemuGPUDevPhysicalOpts{
dev: qemuDevOpts{
busName: bus.name,
devBus: devBus,
devAddr: devAddr,
multifunction: multi,
},
// Generate associated device name by combining main device name and VF ID.
devName: fmt.Sprintf("%s_%s", devName, devAddr),
pciSlotName: iommuSlotName,
vga: false,
vgpu: "",
}
*conf = append(*conf, qemuGPUDevPhysical(&gpuDevPhysicalOpts)...)
}
return nil
})
if err != nil {
return err
}
}
return nil
}
func (d *qemu) addUSBDeviceConfig(usbDev deviceConfig.USBDeviceItem) (monitorHook, error) {
qemuDev := map[string]any{
"id": fmt.Sprintf("%s%s", qemuDeviceIDPrefix, usbDev.DeviceName),
"driver": "usb-host",
"bus": "qemu_usb.0",
}
monHook := func(m *qmp.Monitor) error {
reverter := revert.New()
defer reverter.Fail()
f, err := os.OpenFile(usbDev.HostDevicePath, unix.O_RDWR, 0)
if err != nil {
return fmt.Errorf("Failed to open host device: %w", err)
}
defer logger.WarnOnError(f.Close, "Failed to close file")
info, err := m.SendFileWithFDSet(qemuDev["id"].(string), f, false)
if err != nil {
return fmt.Errorf("Failed to send file descriptor: %w", err)
}
reverter.Add(func() {
_ = m.RemoveFDFromFDSet(qemuDev["id"].(string))
})
qemuDev["hostdevice"] = fmt.Sprintf("/dev/fdset/%d", info.ID)
err = m.AddDevice(qemuDev)
if err != nil {
return fmt.Errorf("Failed to add device: %w", err)
}
reverter.Success()
return nil
}
return monHook, nil
}
func (d *qemu) addTPMDeviceConfig(conf *[]cfg.Section, tpmConfig []deviceConfig.RunConfigItem, fdFiles *[]*os.File) error {
var devName, socketPath string
for _, tpmItem := range tpmConfig {
switch tpmItem.Key {
case "path":
socketPath = tpmItem.Value
case "devName":
devName = tpmItem.Value
}
}
fd, err := unix.Open(socketPath, unix.O_PATH|unix.O_CLOEXEC, 0)
if err != nil {
return err
}
tpmFD := d.addFileDescriptor(fdFiles, os.NewFile(uintptr(fd), socketPath))
tpmDriver := "tpm-tis-device"
if d.architecture == osarch.ARCH_64BIT_INTEL_X86 {
tpmDriver = "tpm-crb"
}
tpmOpts := qemuTPMOpts{
devName: devName,
path: fmt.Sprintf("/proc/self/fd/%d", tpmFD),
driver: tpmDriver,
}
*conf = append(*conf, qemuTPM(&tpmOpts)...)
return nil
}
func (d *qemu) addVmgenDeviceConfig(conf *[]cfg.Section, guid string) error {
vmgenIDOpts := qemuVmgenIDOpts{
guid: guid,
}
*conf = append(*conf, qemuVmgen(&vmgenIDOpts)...)
return nil
}
// pidFilePath returns the path where the qemu process should write its PID.
func (d *qemu) pidFilePath() string {
return filepath.Join(d.RunPath(), "qemu.pid")
}
// pid gets the PID of the running qemu process. Returns 0 if PID file or process not found, and -1 if err non-nil.
func (d *qemu) pid() (int, error) {
pidStr, err := os.ReadFile(d.pidFilePath())
if errors.Is(err, fs.ErrNotExist) {
return 0, nil // PID file has gone.
}
if err != nil {
return -1, err
}
pid, err := strconv.Atoi(strings.TrimSpace(string(pidStr)))
if err != nil {
return -1, err
}
cmdLineProcFilePath := fmt.Sprintf("/proc/%d/cmdline", pid)
cmdLine, err := os.ReadFile(cmdLineProcFilePath)
if err != nil {
return 0, nil // Process has gone.
}
qemuSearchString := []byte("qemu-system")
instUUID := []byte(d.localConfig["volatile.uuid"])
if !bytes.Contains(cmdLine, qemuSearchString) || !bytes.Contains(cmdLine, instUUID) {
return -1, errors.New("PID doesn't match the running process")
}
return pid, nil
}
// forceStop kills the QEMU prorcess if running.
func (d *qemu) forceStop() error {
pid, _ := d.pid()
if pid > 0 {
err := d.killQemuProcess(pid)
if err != nil {
return fmt.Errorf("Failed to stop VM process %d: %w", pid, err)
}
}
return nil
}
// Stop the VM.
func (d *qemu) Stop(stateful bool) error {
d.logger.Debug("Stop started", logger.Ctx{"stateful": stateful})
defer d.logger.Debug("Stop finished", logger.Ctx{"stateful": stateful})
// Must be run prior to creating the operation lock.
// Allow to proceed if statusCode is Error or Frozen as we may need to forcefully kill the QEMU process.
// Also Stop() is called from migrateSendLive in some cases, and instance status will be Frozen then.
statusCode := d.statusCode()
if !d.isRunningStatusCode(statusCode) && statusCode != api.Error && statusCode != api.Frozen {
return ErrInstanceIsStopped
}
// Check for stateful.
if stateful {
// Confirm the instance has stateful migration enabled.
if !d.CanLiveMigrate() {
return errors.New("Stateful stop requires migration.stateful to be set to true")
}
// Confirm the instance has sufficient reserved state space.
err := d.checkStateStorage()
if err != nil {
return err
}
}
// Attempt to save the console log from ring buffer before the instance is stopped. Must be run prior to creating the operation lock.
_, _ = d.ConsoleLog()
// Setup a new operation.
// Allow inheriting of ongoing restart or restore operation (we are called from restartCommon and Restore).
// Don't allow reuse when creating a new stop operation. This prevents other operations from interfering.
// Allow reuse of a reusable ongoing stop operation as Shutdown() may be called first, which allows reuse
// of its operations. This allow for Stop() to inherit from Shutdown() where instance is stuck.
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionStop, []operationlock.Action{operationlock.ActionRestart, operationlock.ActionRestore, operationlock.ActionMigrate}, false, true)
if err != nil {
if errors.Is(err, operationlock.ErrNonReusuableSucceeded) {
// An existing matching operation has now succeeded, return.
return nil
}
return err
}
// Connect to the monitor.
monitor, err := d.qmpConnect()
if err != nil {
d.logger.Warn("Failed connecting to monitor, forcing stop", logger.Ctx{"err": err})
// If we fail to connect, it's most likely because the VM is already off, but it could also be
// because the qemu process is not responding, check if process still exists and kill it if needed.
err = d.forceStop()
if err != nil {
op.Done(err)
return err
}
// Wait for QEMU process to exit and perform device cleanup.
// Treat as host-qmp-quit so autoRestart isn't triggered for a user-requested force stop.
err = d.onStop("stop", qmp.EventVMShutdownReasonQuit)
if err != nil {
op.Done(err)
return err
}
op.Done(nil)
return nil
}
// Handle stateful stop.
if stateful {
// Dump the state.
err = d.saveState(monitor)
if err != nil {
op.Done(err)
return err
}
d.stateful = true
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
return tx.UpdateInstanceStatefulFlag(ctx, d.id, true)
})
if err != nil {
op.Done(err)
return err
}
}
// Get the wait channel.
chDisconnect, err := monitor.Wait()
if err != nil {
d.logger.Warn("Failed getting monitor disconnection channel, forcing stop", logger.Ctx{"err": err})
err = d.forceStop()
if err != nil {
op.Done(err)
return err
}
} else {
// Request the VM stop immediately.
err = monitor.Quit()
if err != nil {
d.logger.Warn("Failed sending monitor quit command, forcing stop", logger.Ctx{"err": err})
err = d.forceStop()
if err != nil {
op.Done(err)
return err
}
}
// Wait for QEMU to exit (can take a while if pending I/O).
// As this is a forceful stop of the VM we don't wait as long as during a clean shutdown because
// the QEMU process may be not responding correctly.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
select {
case <-chDisconnect:
case <-ctx.Done():
d.logger.Warn("Timed out waiting for monitor to disconnect, forcing stop")
err = d.forceStop()
if err != nil {
op.Done(err)
return err
}
}
}
// Wait for operation lock to be Done. This is normally completed by onStop which picks up the same
// operation lock and then marks it as Done after the instance stops and the devices have been cleaned up.
// However if the operation has failed for another reason we will collect the error here.
err = op.Wait(context.Background())
status := d.statusCode()
if status != api.Stopped {
errPrefix := fmt.Errorf("Failed stopping instance, status is %q", status)
if err != nil {
return fmt.Errorf("%s: %w", errPrefix.Error(), err)
}
return errPrefix
}
// Now handle errors from stop sequence and return to caller if wasn't completed cleanly.
if err != nil {
return err
}
return nil
}
// Unfreeze restores the instance to running.
func (d *qemu) Unfreeze() error {
// Connect to the monitor.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
// Send the cont command.
err = monitor.Start()
if err != nil {
return err
}
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceResumed.Event(d, nil))
return nil
}
// IsPrivileged does not apply to virtual machines. Always returns false.
func (d *qemu) IsPrivileged() bool {
return false
}
// snapshot creates a snapshot of the instance.
func (d *qemu) snapshot(name string, expiry time.Time, stateful bool) error {
var err error
var monitor *qmp.Monitor
// Deal with state.
if stateful {
// Confirm the instance has stateful migration enabled.
if !d.CanLiveMigrate() {
return errors.New("Stateful snapshot requires migration.stateful to be set to true")
}
// Confirm the instance has sufficient reserved state space.
err = d.checkStateStorage()
if err != nil {
return err
}
// Quick checks.
if !d.IsRunning() {
return errors.New("Unable to create a stateful snapshot. The instance isn't running")
}
// Connect to the monitor.
monitor, err = d.qmpConnect()
if err != nil {
return err
}
// Dump the state.
err = d.saveState(monitor)
if err != nil {
return err
}
}
// Create the snapshot.
err = d.snapshotCommon(d, name, expiry, stateful)
if err != nil {
return err
}
// Resume the VM once the disk state has been saved.
if stateful {
// Remove the state from the main volume.
err = os.Remove(d.StatePath())
if err != nil {
return err
}
err = monitor.Start()
if err != nil {
return err
}
}
return nil
}
// Snapshot takes a new snapshot.
func (d *qemu) Snapshot(name string, expiry time.Time, stateful bool) error {
return d.snapshot(name, expiry, stateful)
}
// Restore restores an instance snapshot.
func (d *qemu) Restore(source instance.Instance, stateful bool, diskOnly bool) error {
op, err := operationlock.Create(d.Project().Name, d.Name(), d.op, operationlock.ActionRestore, false, false)
if err != nil {
return fmt.Errorf("Failed to create instance restore operation: %w", err)
}
defer op.Done(nil)
var ctxMap logger.Ctx
// Load the storage driver.
pool, err := storagePools.LoadByInstance(d.state, d)
if err != nil {
op.Done(err)
return err
}
err = pool.CanRestoreInstanceSnapshot(d, source)
if err != nil {
op.Done(err)
return err
}
// Stop the instance.
wasRunning := false
if d.IsRunning() {
wasRunning = true
ephemeral := d.IsEphemeral()
if ephemeral {
// Unset ephemeral flag.
args := db.InstanceArgs{
Architecture: d.Architecture(),
Config: d.LocalConfig(),
Description: d.Description(),
Devices: d.LocalDevices(),
Ephemeral: false,
Profiles: d.Profiles(),
Project: d.Project().Name,
Type: d.Type(),
Snapshot: d.IsSnapshot(),
}
err := d.Update(args, false)
if err != nil {
op.Done(err)
return err
}
// On function return, set the flag back on.
defer func() {
args.Ephemeral = ephemeral
_ = d.Update(args, false)
}()
}
// This will unmount the instance storage.
err := d.Stop(false)
if err != nil {
op.Done(err)
return err
}
// Refresh the operation as that one is now complete.
op, err = operationlock.Create(d.Project().Name, d.Name(), d.op, operationlock.ActionRestore, false, false)
if err != nil {
return fmt.Errorf("Failed to create instance restore operation: %w", err)
}
defer op.Done(nil)
}
ctxMap = logger.Ctx{
"created": d.creationDate,
"ephemeral": d.ephemeral,
"used": d.lastUsedDate,
"source": source.Name(),
}
d.logger.Info("Restoring instance", ctxMap)
// Restore the rootfs.
err = pool.RestoreInstanceSnapshot(d, source, nil)
if err != nil {
op.Done(err)
return err
}
args := db.InstanceArgs{}
if !diskOnly {
// Restore the configuration.
args = db.InstanceArgs{
Architecture: source.Architecture(),
Config: source.LocalConfig(),
Description: source.Description(),
Devices: source.LocalDevices(),
Ephemeral: source.IsEphemeral(),
Profiles: source.Profiles(),
Project: source.Project().Name,
Type: source.Type(),
Snapshot: source.IsSnapshot(),
}
} else {
args = db.InstanceArgs{
Architecture: d.Architecture(),
Config: d.LocalConfig(),
Description: d.Description(),
Devices: d.LocalDevices(),
Ephemeral: d.IsEphemeral(),
Profiles: d.Profiles(),
Project: d.Project().Name,
Type: d.Type(),
Snapshot: d.IsSnapshot(),
}
args.Config["volatile.uuid.generation"] = source.LocalConfig()["volatile.uuid.generation"]
}
// Don't pass as user-requested as there's no way to fix a bad config.
// This will call d.UpdateBackupFile() to ensure snapshot list is up to date.
err = d.Update(args, false)
if err != nil {
op.Done(err)
return err
}
d.stateful = stateful
// Restart the instance.
if wasRunning || stateful {
d.logger.Debug("Starting instance after snapshot restore")
err := d.Start(stateful)
if err != nil {
op.Done(err)
return err
}
}
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceRestored.Event(d, map[string]any{"snapshot": source.Name()}))
d.logger.Info("Restored instance", ctxMap)
return nil
}
// Rename the instance. Accepts an argument to enable applying deferred TemplateTriggerRename.
func (d *qemu) Rename(newName string, applyTemplateTrigger bool) error {
oldName := d.Name()
ctxMap := logger.Ctx{
"created": d.creationDate,
"ephemeral": d.ephemeral,
"used": d.lastUsedDate,
"newname": newName,
}
d.logger.Info("Renaming instance", ctxMap)
// Quick checks.
err := instance.ValidName(newName, d.IsSnapshot())
if err != nil {
return err
}
if d.IsRunning() {
return errors.New("Renaming of running instance not allowed")
}
// Clean things up.
d.cleanup()
pool, err := storagePools.LoadByInstance(d.state, d)
if err != nil {
return fmt.Errorf("Failed loading instance storage pool: %w", err)
}
if d.IsSnapshot() {
_, newSnapName, _ := api.GetParentAndSnapshotName(newName)
err = pool.RenameInstanceSnapshot(d, newSnapName, nil)
if err != nil {
return fmt.Errorf("Rename instance snapshot: %w", err)
}
} else {
err = pool.RenameInstance(d, newName, nil)
if err != nil {
return fmt.Errorf("Rename instance: %w", err)
}
if applyTemplateTrigger {
err = d.DeferTemplateApply(instance.TemplateTriggerRename)
if err != nil {
return err
}
}
}
if !d.IsSnapshot() {
var results []string
err := d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
var err error
// Rename all the instance snapshot database entries.
results, err = tx.GetInstanceSnapshotsNames(ctx, d.project.Name, oldName)
if err != nil {
d.logger.Error("Failed to get instance snapshots", ctxMap)
return fmt.Errorf("Failed to get instance snapshots: Failed getting instance snapshot names: %w", err)
}
for _, sname := range results {
// Rename the snapshot.
oldSnapName := strings.SplitN(sname, internalInstance.SnapshotDelimiter, 2)[1]
baseSnapName := filepath.Base(sname)
err := dbCluster.RenameInstanceSnapshot(ctx, tx.Tx(), d.project.Name, oldName, oldSnapName, baseSnapName)
if err != nil {
d.logger.Error("Failed renaming snapshot", ctxMap)
return err
}
}
return nil
})
if err != nil {
return err
}
}
// Rename the instance database entry.
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
if d.IsSnapshot() {
oldParts := strings.SplitN(oldName, internalInstance.SnapshotDelimiter, 2)
newParts := strings.SplitN(newName, internalInstance.SnapshotDelimiter, 2)
return dbCluster.RenameInstanceSnapshot(ctx, tx.Tx(), d.project.Name, oldParts[0], oldParts[1], newParts[1])
}
return dbCluster.RenameInstance(ctx, tx.Tx(), d.project.Name, oldName, newName)
})
if err != nil {
d.logger.Error("Failed renaming instance", ctxMap)
return err
}
// Rename the logging path.
newFullName := project.Instance(d.Project().Name, d.Name())
_ = os.RemoveAll(internalUtil.LogPath(newFullName))
if util.PathExists(d.LogPath()) {
err := os.Rename(d.LogPath(), internalUtil.LogPath(newFullName))
if err != nil {
d.logger.Error("Failed renaming instance", ctxMap)
return err
}
}
// Rename the runtime path.
newFullName = project.Instance(d.Project().Name, d.Name())
_ = os.RemoveAll(internalUtil.RunPath(newFullName))
if util.PathExists(d.RunPath()) {
err := os.Rename(d.RunPath(), internalUtil.RunPath(newFullName))
if err != nil {
d.logger.Error("Failed renaming instance", ctxMap)
return err
}
}
reverter := revert.New()
defer reverter.Fail()
// Set the new name in the struct.
d.name = newName
reverter.Add(func() { d.name = oldName })
// Rename the backups.
backups, err := d.Backups()
if err != nil {
return err
}
for _, backup := range backups {
b := backup
oldName := b.Name()
backupName := strings.Split(oldName, "/")[1]
newName := fmt.Sprintf("%s/%s", newName, backupName)
err = b.Rename(newName)
if err != nil {
return err
}
reverter.Add(func() { _ = b.Rename(oldName) })
}
// Update lease files.
err = network.UpdateDNSMasqStatic(d.state, "")
if err != nil {
return err
}
// Reset cloud-init instance-id (causes a re-run on name changes).
if !d.IsSnapshot() {
err = d.resetInstanceID()
if err != nil {
return err
}
}
// Update the backup file.
err = d.UpdateBackupFile()
if err != nil {
return err
}
d.logger.Info("Renamed instance", ctxMap)
if d.isSnapshot {
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotRenamed.Event(d, map[string]any{"old_name": oldName}))
} else {
err = d.state.Authorizer.RenameInstance(d.state.ShutdownCtx, d.project.Name, oldName, newName)
if err != nil {
logger.Error("Failed to rename instance in authorizer", logger.Ctx{"old_name": oldName, "new_name": newName, "project": d.project.Name, "error": err})
}
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceRenamed.Event(d, map[string]any{"old_name": oldName}))
}
reverter.Success()
return nil
}
// Indirection to detachDisk.
func qemuDetachDisk(s *state.State, id int) func(string) error {
return func(name string) error {
inst, err := instance.LoadByID(s, id)
if err != nil {
return err
}
qemuInst, ok := inst.(*qemu)
if !ok {
return fmt.Errorf("Couldn't assert QEMU object from interface")
}
return qemuInst.detachDisk(name)
}
}
// Detach a disk from the instance.
func (d *qemu) detachDisk(name string) error {
diskName := linux.PathNameDecode(strings.TrimPrefix(name, qemuDeviceIDPrefix))
// Load and detach the disk.
config, ok := d.expandedDevices[diskName]
if !ok {
return fmt.Errorf("Couldn't find device %s", diskName)
}
dev, err := d.deviceLoad(d, diskName, config, false)
if err != nil {
return err
}
err = d.deviceStop(dev, true, "")
if err != nil {
return err
}
// Check if it's a special device or an inherited device for which we can't save state.
_, ok = d.localDevices[diskName]
if !ok || slices.Contains([]string{"agent:config", "cloud-init:config"}, config["source"]) {
// Record that the instance devices got modified and a full reset will be needed to get a consistent state.
err = d.VolatileSet(map[string]string{
"volatile.vm.needs_reset": "true",
})
if err != nil {
return err
}
return nil
}
// Record the device as detached.
d.localDevices[diskName]["attached"] = "false"
return d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
devices, err := dbCluster.APIToDevices(d.localDevices.CloneNative())
if err != nil {
return err
}
return dbCluster.UpdateInstanceDevices(ctx, tx.Tx(), int64(d.id), devices)
})
}
// Update the instance config.
func (d *qemu) Update(args db.InstanceArgs, userRequested bool) error {
// Setup a new operation.
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionUpdate, []operationlock.Action{operationlock.ActionRestart, operationlock.ActionRestore}, false, false)
if err != nil {
return fmt.Errorf("Failed to create instance update operation: %w", err)
}
defer op.Done(nil)
// Setup the reverter.
reverter := revert.New()
defer reverter.Fail()
// Set sane defaults for unset keys.
if args.Project == "" {
args.Project = api.ProjectDefaultName
}
if args.Architecture == 0 {
args.Architecture = d.architecture
}
if args.Config == nil {
args.Config = map[string]string{}
}
if args.Devices == nil {
args.Devices = deviceConfig.Devices{}
}
if args.Profiles == nil {
args.Profiles = []api.Profile{}
}
if userRequested {
// Validate the new config.
err := instance.ValidConfig(d.state.OS, args.Config, false, d.dbType)
if err != nil {
return fmt.Errorf("Invalid config: %w", err)
}
// Validate the new devices without using expanded devices validation (expensive checks disabled).
err = instance.ValidDevices(d.state, d.project, d.Type(), args.Devices, nil)
if err != nil {
return fmt.Errorf("Invalid devices: %w", err)
}
}
var profiles []string
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
// Validate the new profiles.
profiles, err = tx.GetProfileNames(ctx, args.Project)
return err
})
if err != nil {
return fmt.Errorf("Failed to get profiles: %w", err)
}
checkedProfiles := []string{}
for _, profile := range args.Profiles {
if !slices.Contains(profiles, profile.Name) {
return fmt.Errorf("Requested profile '%s' doesn't exist", profile.Name)
}
if slices.Contains(checkedProfiles, profile.Name) {
return errors.New("Duplicate profile found in request")
}
checkedProfiles = append(checkedProfiles, profile.Name)
}
// Validate the new architecture.
if args.Architecture != 0 {
_, err = osarch.ArchitectureName(args.Architecture)
if err != nil {
return fmt.Errorf("Invalid architecture ID: %s", err)
}
}
// Get a copy of the old configuration.
oldDescription := d.Description()
oldArchitecture := 0
err = util.DeepCopy(&d.architecture, &oldArchitecture)
if err != nil {
return err
}
oldEphemeral := false
err = util.DeepCopy(&d.ephemeral, &oldEphemeral)
if err != nil {
return err
}
oldExpandedDevices := deviceConfig.Devices{}
err = util.DeepCopy(&d.expandedDevices, &oldExpandedDevices)
if err != nil {
return err
}
oldExpandedConfig := map[string]string{}
err = util.DeepCopy(&d.expandedConfig, &oldExpandedConfig)
if err != nil {
return err
}
oldLocalDevices := deviceConfig.Devices{}
err = util.DeepCopy(&d.localDevices, &oldLocalDevices)
if err != nil {
return err
}
oldLocalConfig := map[string]string{}
err = util.DeepCopy(&d.localConfig, &oldLocalConfig)
if err != nil {
return err
}
oldProfiles := []api.Profile{}
err = util.DeepCopy(&d.profiles, &oldProfiles)
if err != nil {
return err
}
oldExpiryDate := d.expiryDate
// Revert local changes if update fails.
reverter.Add(func() {
d.description = oldDescription
d.architecture = oldArchitecture
d.ephemeral = oldEphemeral
d.expandedConfig = oldExpandedConfig
d.expandedDevices = oldExpandedDevices
d.localConfig = oldLocalConfig
d.localDevices = oldLocalDevices
d.profiles = oldProfiles
d.expiryDate = oldExpiryDate
})
// Apply the various changes to local vars.
d.description = args.Description
d.architecture = args.Architecture
d.ephemeral = args.Ephemeral
d.localConfig = args.Config
d.localDevices = args.Devices
d.profiles = args.Profiles
d.expiryDate = args.ExpiryDate
// Expand the config.
err = d.expandConfig()
if err != nil {
return err
}
// Diff the configurations.
changedConfig := []string{}
for key := range oldExpandedConfig {
if oldExpandedConfig[key] != d.expandedConfig[key] {
if !slices.Contains(changedConfig, key) {
changedConfig = append(changedConfig, key)
}
}
}
for key := range d.expandedConfig {
if oldExpandedConfig[key] != d.expandedConfig[key] {
if !slices.Contains(changedConfig, key) {
changedConfig = append(changedConfig, key)
}
}
}
// Diff the devices.
removeDevices, addDevices, updateDevices, allUpdatedKeys := oldExpandedDevices.Update(d.expandedDevices, func(oldDevice deviceConfig.Device, newDevice deviceConfig.Device) []string {
// This function needs to return a list of fields that are excluded from differences
// between oldDevice and newDevice. The result of this is that as long as the
// devices are otherwise identical except for the fields returned here, then the
// device is considered to be being "updated" rather than "added & removed".
oldDevType, err := device.LoadByType(d.state, d.Project().Name, oldDevice)
if err != nil {
return []string{} // Couldn't create Device, so this cannot be an update.
}
newDevType, err := device.LoadByType(d.state, d.Project().Name, newDevice)
if err != nil {
return []string{} // Couldn't create Device, so this cannot be an update.
}
// Detached devices need to be fully recreated on update so that the update logic doesn't
// try to access non-existing QEMU devices.
if !util.IsTrueOrEmpty(oldDevice["attached"]) {
return []string{}
}
return newDevType.UpdatableFields(oldDevType)
})
// Prevent adding or updating device initial configuration.
if util.StringPrefixInSlice("initial.", allUpdatedKeys) {
for devName, newDev := range addDevices {
for k, newVal := range newDev {
if !strings.HasPrefix(k, "initial.") {
continue
}
if newDev["pool"] != "" && newDev["path"] != "/" && strings.Contains(newDev["source"], "/") {
continue
}
oldDev, ok := removeDevices[devName]
if !ok {
return errors.New("New device with initial configuration cannot be added once the instance is created")
}
oldVal, ok := oldDev[k]
if !ok {
return errors.New("Device initial configuration cannot be added once the instance is created")
}
// If newVal is an empty string it means the initial configuration
// has been removed.
if newVal != "" && newVal != oldVal {
return errors.New("Device initial configuration cannot be modified once the instance is created")
}
}
}
}
if userRequested {
// Do some validation of the config diff (allows mixed instance types for profiles).
err = instance.ValidConfig(d.state.OS, d.expandedConfig, true, instancetype.Any)
if err != nil {
return fmt.Errorf("Invalid expanded config: %w", err)
}
// Do full expanded validation of the devices diff.
err = instance.ValidDevices(d.state, d.project, d.Type(), d.localDevices, d.expandedDevices)
if err != nil {
return fmt.Errorf("Invalid expanded devices: %w", err)
}
// Validate root device
_, oldRootDev, oldErr := internalInstance.GetRootDiskDevice(oldExpandedDevices.CloneNative())
_, newRootDev, newErr := internalInstance.GetRootDiskDevice(d.expandedDevices.CloneNative())
if oldErr == nil && newErr == nil && oldRootDev["pool"] != newRootDev["pool"] {
return fmt.Errorf("Cannot update root disk device pool name to %q", newRootDev["pool"])
}
// Ensure the instance has a root disk.
if newErr != nil {
return fmt.Errorf("Invalid root disk device: %w", newErr)
}
}
// If apparmor changed, re-validate the apparmor profile (even if not running).
if slices.Contains(changedConfig, "raw.apparmor") {
qemuPath, _, err := d.qemuArchConfig(d.architecture)
if err != nil {
return err
}
err = apparmor.InstanceValidate(d.state.OS, d, []string{qemuPath})
if err != nil {
return fmt.Errorf("Parse AppArmor profile: %w", err)
}
}
isRunning := d.IsRunning()
// Use the device interface to apply update changes.
err = d.devicesUpdate(d, removeDevices, addDevices, updateDevices, oldExpandedDevices, isRunning, userRequested)
if err != nil {
return err
}
if isRunning {
// Only certain keys can be changed on a running VM.
liveUpdateKeys := []string{
"cluster.evacuate",
"limits.memory",
"security.agent.metrics",
"security.csm",
"security.protection.delete",
"security.guestapi",
"security.secureboot",
}
liveUpdateKeyPrefixes := []string{
"boot.",
"cloud-init.",
"environment.",
"image.",
"snapshots.",
"user.",
"volatile.",
}
isLiveUpdatable := func(key string) bool {
// Skip container config keys for VMs
_, ok := internalInstance.InstanceConfigKeysContainer[key]
if ok {
return true
}
if key == "limits.cpu" {
return d.architectureSupportsCPUHotplug()
}
if slices.Contains(liveUpdateKeys, key) {
return true
}
if util.StringHasPrefix(key, liveUpdateKeyPrefixes...) {
return true
}
if key == "limits.memory.oom_priority" {
return true
}
return false
}
// Check only keys that support live update have changed.
for _, key := range changedConfig {
if !isLiveUpdatable(key) {
return fmt.Errorf("Key %q cannot be updated when VM is running", key)
}
}
// Mark the VM as needing a full reset on next reboot.
err = d.VolatileSet(map[string]string{
"volatile.vm.needs_reset": "true",
})
if err != nil {
return err
}
// Apply live update for each key.
for _, key := range changedConfig {
value := d.expandedConfig[key]
switch key {
case "limits.cpu":
oldValue := oldExpandedConfig["limits.cpu"]
if oldValue != "" {
_, err := strconv.Atoi(oldValue)
if err != nil {
return fmt.Errorf("Cannot update key %q when using CPU pinning or an explicit CPU topology and the VM is running", key)
}
}
// If the key is being unset, set it to default value.
if value == "" {
value = "1"
}
limit, err := strconv.Atoi(value)
if err != nil {
return errors.New("Cannot change to CPU pinning or an explicit CPU topology when the VM is running")
}
// Hotplug the CPUs.
err = d.setCPUs(nil, limit)
if err != nil {
return fmt.Errorf("Failed updating cpu limit: %w", err)
}
case "limits.memory":
err = d.updateMemoryLimit(value)
if err != nil {
if err != nil {
return fmt.Errorf("Failed updating memory limit: %w", err)
}
}
case "security.csm":
// Defer rebuilding nvram until next start.
d.localConfig["volatile.apply_nvram"] = "true"
case "security.secureboot":
// Defer rebuilding nvram until next start.
d.localConfig["volatile.apply_nvram"] = "true"
case "security.guestapi":
err = d.advertiseVsockAddress()
if err != nil {
return err
}
case "limits.memory.oom_priority":
// Configure the OOM priority.
err = d.setOOMPriority(d.InitPID())
if err != nil {
d.logger.Warn("Failed to set OOM priority", logger.Ctx{
"err": err,
"instance": d.Name(),
"project": d.Project().Name,
})
}
}
}
}
// Clear the "volatile.cpu.nodes" if needed.
d.ClearLimitsCPUNodes(changedConfig)
if d.architectureSupportsUEFI(d.architecture) && (slices.Contains(changedConfig, "security.secureboot") || slices.Contains(changedConfig, "security.csm")) {
// setupNvram() requires instance's config volume to be mounted.
// The easiest way to detect that is to check if instance is running.
// TODO: extend storage API to be able to check if volume is already mounted?
if !isRunning {
// Mount the instance's config volume.
_, err := d.mount()
if err != nil {
return err
}
defer logger.WarnOnError(d.unmount, "Failed to unmount instance")
}
// Re-generate the NVRAM.
err = d.setupNvram()
if err != nil {
return err
}
}
// Re-generate the instance-id if needed.
if !d.IsSnapshot() && d.needsNewInstanceID(changedConfig, oldExpandedDevices) {
err = d.resetInstanceID()
if err != nil {
return err
}
}
// Finally, apply the changes to the database.
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
// Snapshots should update only their descriptions and expiry date.
if d.IsSnapshot() {
return tx.UpdateInstanceSnapshot(d.id, d.description, d.expiryDate)
}
object, err := dbCluster.GetInstance(ctx, tx.Tx(), d.project.Name, d.name)
if err != nil {
return err
}
object.Description = d.description
object.Architecture = d.architecture
object.Ephemeral = d.ephemeral
object.ExpiryDate = sql.NullTime{Time: d.expiryDate, Valid: true}
err = dbCluster.UpdateInstance(ctx, tx.Tx(), d.project.Name, d.name, *object)
if err != nil {
return err
}
err = dbCluster.UpdateInstanceConfig(ctx, tx.Tx(), int64(object.ID), d.localConfig)
if err != nil {
return err
}
devices, err := dbCluster.APIToDevices(d.localDevices.CloneNative())
if err != nil {
return err
}
err = dbCluster.UpdateInstanceDevices(ctx, tx.Tx(), int64(object.ID), devices)
if err != nil {
return err
}
profileNames := make([]string, 0, len(d.profiles))
for _, profile := range d.profiles {
profileNames = append(profileNames, profile.Name)
}
return dbCluster.UpdateInstanceProfiles(ctx, tx.Tx(), object.ID, object.Project, profileNames)
})
if err != nil {
return fmt.Errorf("Failed to update database: %w", err)
}
err = d.UpdateBackupFile()
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("Failed to write backup file: %w", err)
}
// Changes have been applied and recorded, do not revert if an error occurs from here.
reverter.Success()
if isRunning {
// Send devIncus notifications only for user.* key changes
for _, key := range changedConfig {
if !strings.HasPrefix(key, "user.") {
continue
}
msg := map[string]any{
"key": key,
"old_value": oldExpandedConfig[key],
"value": d.expandedConfig[key],
}
err = d.devIncusEventSend("config", msg)
if err != nil {
return err
}
}
// Device changes
for k, m := range removeDevices {
msg := map[string]any{
"action": "removed",
"name": k,
"config": m,
}
err = d.devIncusEventSend("device", msg)
if err != nil {
return err
}
}
for k, m := range updateDevices {
msg := map[string]any{
"action": "updated",
"name": k,
"config": m,
}
err = d.devIncusEventSend("device", msg)
if err != nil {
return err
}
}
for k, m := range addDevices {
msg := map[string]any{
"action": "added",
"name": k,
"config": m,
}
err = d.devIncusEventSend("device", msg)
if err != nil {
return err
}
}
}
if userRequested {
if d.isSnapshot {
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotUpdated.Event(d, nil))
} else {
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceUpdated.Event(d, nil))
}
}
return nil
}
// updateMemoryLimit live updates the VM's memory limit by reszing the balloon device.
func (d *qemu) updateMemoryLimit(newLimit string) error {
if newLimit == "" {
return nil
}
if util.IsTrue(d.expandedConfig["limits.memory.hugepages"]) {
return errors.New("Cannot live update memory limit when using huge pages")
}
// Check new size string is valid and convert to bytes.
newSizeBytes, err := ParseMemoryStr(newLimit)
if err != nil {
return fmt.Errorf("Invalid memory size: %w", err)
}
newSizeMB := newSizeBytes / 1024 / 1024
// Connect to the monitor.
monitor, err := d.qmpConnect()
if err != nil {
return err // The VM isn't running as no monitor socket available.
}
baseSizeBytes, err := monitor.GetMemorySizeBytes()
if err != nil {
return err
}
baseSizeMB := baseSizeBytes / 1024 / 1024
curSizeBytes, err := monitor.GetMemoryBalloonSizeBytes()
if err != nil {
return err
}
curSizeMB := curSizeBytes / 1024 / 1024
if curSizeMB == newSizeMB {
return nil
} else if baseSizeMB < newSizeMB {
if util.IsFalse(d.expandedConfig["limits.memory.hotplug"]) || d.GuestOS() == osinfo.FreeBSD {
return fmt.Errorf("Memory hotplug feature is disabled")
}
// Grab the current memory configuration.
baseMem, maxMem, _, err := monitor.MemoryConfiguration()
if err != nil {
return err
}
// Make sure that we're not exceeding the VM's configured maximum.
if newSizeBytes > maxMem {
return fmt.Errorf("Requested memory total of %s exceeds instance current maximum of %s, restart required", units.GetByteSizeStringIEC(newSizeBytes, 2), units.GetByteSizeStringIEC(maxMem, 2))
}
// Add the memory.
err = d.hotplugMemory(monitor, newSizeBytes-curSizeBytes)
if err != nil {
return err
}
// If migratable, update state information following hotplug.
if d.CanLiveMigrate() {
// Prepare an updated memory topology struct.
memTopology := qemuMemoryTopology{
Base: baseMem,
Max: maxMem,
Extra: []int64{},
}
memDevs, err := monitor.GetMemdev()
if err != nil {
return err
}
memSlots := map[string]int64{}
memSlotsKeys := []string{}
for _, memDev := range memDevs {
// Skip base memory objects.
if memDev.ID == "mem0" || memDev.ID == qemuDefaultRAMObject(d.architecture) {
continue
}
memSlots[memDev.ID] = int64(memDev.Size)
memSlotsKeys = append(memSlotsKeys, memDev.ID)
}
// The list out of QEMU is in random order...
sort.Strings(memSlotsKeys)
for _, k := range memSlotsKeys {
memTopology.Extra = append(memTopology.Extra, memSlots[k])
}
// Update the boot state record.
bs, err := d.getBootState()
if err != nil {
return err
}
bs.MemoryTopology = &memTopology
err = d.saveBootState(*bs)
if err != nil {
return err
}
}
}
// Set effective memory size.
err = monitor.SetMemoryBalloonSizeBytes(newSizeBytes)
if err != nil {
return err
}
// Changing the memory balloon can take time, so poll the effective size to check it has shrunk within 1%
// of the target size, which we then take as success (it may still continue to shrink closer to target).
for range 10 {
curSizeBytes, err = monitor.GetMemoryBalloonSizeBytes()
if err != nil {
return err
}
curSizeMB = curSizeBytes / 1024 / 1024
var diff int64
if curSizeMB < newSizeMB {
diff = newSizeMB - curSizeMB
} else {
diff = curSizeMB - newSizeMB
}
if diff <= (newSizeMB / 100) {
return nil // We reached to within 1% of our target size.
}
time.Sleep(500 * time.Millisecond)
}
return fmt.Errorf("Failed setting memory to %dMiB (currently %dMiB) as it was taking too long", newSizeMB, curSizeMB)
}
// hotplugMemory attaches a memory device to a running VM,
// respecting NUMA node placement and hugepages.
func (d *qemu) hotplugMemory(monitor *qmp.Monitor, sizeBytes int64) error {
// Get CPU information.
cpuInfo, err := d.cpuTopology()
if err != nil {
return err
}
// Fetch memory configuration
cpuOpts, err := d.getCPUOpts(cpuInfo, sizeBytes)
if err != nil {
return err
}
cpuFixedTopology := cpuInfo.VCPUs != nil || cpuInfo.Explicit
// Get CPUs and memory configuration
conf := qemuCPU(cpuOpts, cpuFixedTopology)
memoryObjects := map[int]cfg.Section{}
for _, section := range conf {
// Name is in the form 'object "mem0"', so the last quote needs to be removed.
// This allows proper parsing of the memory object index.
sectionName := section.Name[:len(section.Name)-1]
index, err := extractTrailingNumber(sectionName, "object \"mem")
if err != nil {
continue
}
memoryObjects[index] = section
}
// Find first available memory object index.
nextMemIndex, err := findNextMemoryIndex(monitor)
if err != nil {
return err
}
// Find first available pc-dimm device index.
nextDimmIndex, err := findNextDimmIndex(monitor)
if err != nil {
return err
}
for index, memory := range memoryObjects {
memIndex := nextMemIndex + index
dimmIndex := nextDimmIndex + index
memObj := memoryConfigSectionToMap(&memory)
memObj["id"] = fmt.Sprintf("mem%d", memIndex)
err = monitor.AddObject(memObj)
if err != nil {
return err
}
memDev := map[string]any{
"driver": "pc-dimm",
"id": fmt.Sprintf("dimm%d", dimmIndex),
"memdev": fmt.Sprintf("mem%d", memIndex),
"node": index,
}
err = monitor.AddDevice(memDev)
if err != nil {
return err
}
}
return nil
}
func (d *qemu) removeUnixDevices() error {
// Check that we indeed have devices to remove.
if !util.PathExists(d.DevicesPath()) {
return nil
}
// Load the directory listing.
dents, err := os.ReadDir(d.DevicesPath())
if err != nil {
return err
}
for _, f := range dents {
// Skip non-Unix devices.
if !strings.HasPrefix(f.Name(), "forkmknod.unix.") && !strings.HasPrefix(f.Name(), "unix.") && !strings.HasPrefix(f.Name(), "infiniband.unix.") {
continue
}
// Remove the entry
devicePath := filepath.Join(d.DevicesPath(), f.Name())
err := os.Remove(devicePath)
if err != nil {
d.logger.Error("Failed removing unix device", logger.Ctx{"err": err, "path": devicePath})
}
}
return nil
}
func (d *qemu) removeDiskDevices() error {
// Check that we indeed have devices to remove.
if !util.PathExists(d.DevicesPath()) {
return nil
}
// Load the directory listing.
dents, err := os.ReadDir(d.DevicesPath())
if err != nil {
return err
}
for _, f := range dents {
// Skip non-disk devices
if !strings.HasPrefix(f.Name(), "disk.") {
continue
}
// Always try to unmount the host side.
_ = unix.Unmount(filepath.Join(d.DevicesPath(), f.Name()), unix.MNT_DETACH)
// Remove the entry.
diskPath := filepath.Join(d.DevicesPath(), f.Name())
err := os.Remove(diskPath)
if err != nil {
d.logger.Error("Failed to remove disk device path", logger.Ctx{"err": err, "path": diskPath})
}
}
return nil
}
func (d *qemu) cleanup() {
// Unmount any leftovers
_ = d.removeUnixDevices()
_ = d.removeDiskDevices()
// Remove the security profiles
_ = apparmor.InstanceDelete(d.state.OS, d)
// Remove the devices path
_ = os.Remove(d.DevicesPath())
// Remove the shmounts path
_ = os.RemoveAll(d.ShmountsPath())
}
// cleanupDevices performs any needed device cleanup steps when instance is stopped.
// Must be called before root volume is unmounted.
func (d *qemu) cleanupDevices() {
// Clear up the config drive mount.
err := d.configDriveMountPathClear()
if err != nil {
d.logger.Warn("Failed cleaning up config drive mount", logger.Ctx{"err": err})
}
for _, entry := range d.expandedDevices.Reversed() {
dev, err := d.deviceLoad(d, entry.Name, entry.Config, false)
if err != nil {
if errors.Is(err, device.ErrUnsupportedDevType) {
continue // Skip unsupported device (allows for mixed instance type profiles).
}
// Just log an error, but still allow the device to be stopped if usable device returned.
d.logger.Error("Failed stop validation for device", logger.Ctx{"device": entry.Name, "err": err})
}
// If a usable device was returned from deviceLoad try to stop anyway, even if validation fails.
// This allows for the scenario where a new version has additional validation restrictions
// than older versions and we still need to allow previously valid devices to be stopped even if
// they are no longer considered valid.
if dev != nil {
err = d.deviceStop(dev, false, "")
if err != nil {
d.logger.Error("Failed to stop device", logger.Ctx{"device": dev.Name(), "err": err})
}
}
}
}
func (d *qemu) init() error {
// Compute the expanded config and device list.
err := d.expandConfig()
if err != nil {
return err
}
return nil
}
// Delete the instance.
// cleanupDependencies controls whether dependent resources (e.g. volumes,
// and related state) are removed along with the instance.
// When false, dependencies are preserved (e.g. storage-only moves).
func (d *qemu) Delete(force bool, cleanupDependencies bool) error {
// Setup a new operation.
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionDelete, nil, false, false)
if err != nil {
return fmt.Errorf("Failed to create instance delete operation: %w", err)
}
defer op.Done(nil)
if d.IsRunning() {
return api.StatusErrorf(http.StatusBadRequest, "Instance is running")
}
err = d.delete(force, cleanupDependencies)
if err != nil {
return err
}
// If dealing with a snapshot, refresh the backup file on the parent.
if d.IsSnapshot() {
parentName, _, _ := api.GetParentAndSnapshotName(d.name)
// Load the parent.
parent, err := instance.LoadByProjectAndName(d.state, d.project.Name, parentName)
if err != nil {
return fmt.Errorf("Invalid parent: %w", err)
}
// Update the backup file.
err = parent.UpdateBackupFile()
if err != nil {
return err
}
}
return nil
}
// Delete the instance without creating an operation lock.
func (d *qemu) delete(force bool, cleanupDependencies bool) error {
ctxMap := logger.Ctx{
"created": d.creationDate,
"ephemeral": d.ephemeral,
"used": d.lastUsedDate,
}
if d.isSnapshot {
d.logger.Info("Deleting instance snapshot", ctxMap)
} else {
d.logger.Info("Deleting instance", ctxMap)
}
// Check if instance is delete protected.
if !force && util.IsTrue(d.expandedConfig["security.protection.delete"]) && !d.IsSnapshot() {
return errors.New("Instance is protected")
}
// Delete any persistent warnings for instance.
err := d.warningsDelete()
if err != nil {
return err
}
// Attempt to initialize storage interface for the instance.
pool, err := d.getStoragePool()
if err != nil && !response.IsNotFoundError(err) {
return err
} else if pool != nil {
if d.IsSnapshot() {
// Remove snapshot volume and database record.
err = pool.DeleteInstanceSnapshot(d, nil)
if err != nil {
return err
}
} else {
// Remove all snapshots.
err := d.deleteSnapshots(func(snapInst instance.Instance) error {
return snapInst.(*qemu).delete(true, cleanupDependencies) // Internal delete function that doesn't lock.
})
if err != nil {
return fmt.Errorf("Failed deleting instance snapshots: %w", err)
}
// Remove the storage volume and database records.
err = pool.DeleteInstance(d, nil)
if err != nil {
return err
}
if cleanupDependencies {
// Delete all dependent volumes associated with this instance.
err = d.ForEachDependentDiskType(func(dev deviceConfig.DeviceNamed) error {
// Load the pool for the disk.
diskPool, err := storagePools.LoadByName(d.state, dev.Config["pool"])
if err != nil {
return fmt.Errorf("Failed loading storage pool: %w", err)
}
volName, _ := internalInstance.SplitVolumeSource(dev.Config["source"])
err = diskPool.DeleteCustomVolume(d.Project().Name, volName, nil)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
}
}
}
// Perform other cleanup steps if not snapshot.
if !d.IsSnapshot() {
// Remove all backups.
backups, err := d.Backups()
if err != nil {
return err
}
for _, backup := range backups {
err = backup.Delete()
if err != nil {
return err
}
}
// Run device removal function for each device.
d.devicesRemove(d, cleanupDependencies)
// Clean things up.
d.cleanup()
}
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
// Remove the database record of the instance or snapshot instance.
return tx.DeleteInstance(ctx, d.Project().Name, d.Name())
})
if err != nil {
d.logger.Error("Failed deleting instance entry", logger.Ctx{"project": d.Project().Name})
return err
}
if d.isSnapshot {
d.logger.Info("Deleted instance snapshot", ctxMap)
} else {
d.logger.Info("Deleted instance", ctxMap)
}
if d.isSnapshot {
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotDeleted.Event(d, nil))
} else {
err = d.state.Authorizer.DeleteInstance(d.state.ShutdownCtx, d.project.Name, d.Name())
if err != nil {
logger.Error("Failed to remove instance from authorizer", logger.Ctx{"name": d.Name(), "project": d.project.Name, "error": err})
}
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceDeleted.Event(d, nil))
}
return nil
}
// Export publishes the instance.
func (d *qemu) Export(metaWriter io.Writer, rootfsWriter io.Writer, properties map[string]string, expiration time.Time, tracker *ioprogress.ProgressTracker) (*api.ImageMetadata, error) {
ctxMap := logger.Ctx{
"created": d.creationDate,
"ephemeral": d.ephemeral,
"used": d.lastUsedDate,
}
if d.IsRunning() {
return nil, errors.New("Cannot export a running instance as an image")
}
d.logger.Info("Exporting instance", ctxMap)
// Start the storage.
mountInfo, err := d.mount()
if err != nil {
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
defer logger.WarnOnError(d.unmount, "Failed to unmount instance")
// Create the tarball.
metaTarWriter := instancewriter.NewInstanceTarWriter(metaWriter, nil)
// Path inside the tar image is the pathname starting after cDir.
cDir := d.Path()
offset := len(cDir) + 1
writeToMetaTar := func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
err = metaTarWriter.WriteFile(path[offset:], path, fi, false)
if err != nil {
d.logger.Debug("Error tarring up", logger.Ctx{"path": path, "err": err})
return err
}
return nil
}
// Get the instance's architecture.
var arch string
if d.IsSnapshot() {
parentName, _, _ := api.GetParentAndSnapshotName(d.name)
parent, err := instance.LoadByProjectAndName(d.state, d.project.Name, parentName)
if err != nil {
_ = metaTarWriter.Close()
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
arch, _ = osarch.ArchitectureName(parent.Architecture())
} else {
arch, _ = osarch.ArchitectureName(d.architecture)
}
if arch == "" {
arch, err = osarch.ArchitectureName(d.state.OS.Architectures[0])
if err != nil {
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
}
// Generate metadata.yaml.
meta := api.ImageMetadata{}
fnam := filepath.Join(cDir, "metadata.yaml")
if util.PathExists(fnam) {
// Parse the metadata.
content, err := os.ReadFile(fnam)
if err != nil {
_ = metaTarWriter.Close()
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
err = yaml.Load(content, &meta)
if err != nil {
_ = metaTarWriter.Close()
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
}
// Fill in the metadata.
meta.Architecture = arch
meta.CreationDate = time.Now().UTC().Unix()
if meta.Properties == nil {
meta.Properties = map[string]string{}
}
maps.Copy(meta.Properties, properties)
if !expiration.IsZero() {
meta.ExpiryDate = expiration.UTC().Unix()
}
// Write the new metadata.yaml.
tempDir, err := os.MkdirTemp("", "incus_metadata_")
if err != nil {
_ = metaTarWriter.Close()
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
defer logger.WarnOnError(func() error { return os.RemoveAll(tempDir) }, "Failed to remove temporary directory")
data, err := yaml.Dump(&meta, yaml.WithV2Defaults())
if err != nil {
_ = metaTarWriter.Close()
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
fnam = filepath.Join(tempDir, "metadata.yaml")
err = os.WriteFile(fnam, data, 0o644)
if err != nil {
_ = metaTarWriter.Close()
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
// Add metadata.yaml to the tarball.
fi, err := os.Lstat(fnam)
if err != nil {
_ = metaTarWriter.Close()
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
tmpOffset := len(filepath.Dir(fnam)) + 1
err = metaTarWriter.WriteFile(fnam[tmpOffset:], fnam, fi, false)
if err != nil {
_ = metaTarWriter.Close()
d.logger.Debug("Error writing to tarfile", logger.Ctx{"err": err})
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
// Convert from raw to qcow2 and add to tarball.
tmpPath, err := os.MkdirTemp(internalUtil.VarPath("images"), "incus_export_")
if err != nil {
return nil, err
}
defer logger.WarnOnError(func() error { return os.RemoveAll(tmpPath) }, "Failed to remove temporary directory")
if mountInfo.DiskPath == "" {
return nil, errors.New("No disk path available from mount")
}
fPath := fmt.Sprintf("%s/rootfs.img", tmpPath)
// On some storage drivers (lvmcluster), the volume holds a qcow2 container rather than raw data.
srcFormat := "raw"
isQcow2, err := d.isQCOW2(mountInfo.DiskPath)
if err != nil {
return nil, fmt.Errorf("Failed checking disk format: %w", err)
}
if isQcow2 {
srcFormat = storageDrivers.BlockVolumeTypeQcow2
}
// Convert to qcow2 image.
cmd := []string{
"nice", "-n19", // Run with low priority to reduce CPU impact on other processes.
"qemu-img", "convert", "-p", "-f", srcFormat, "-O", "qcow2",
}
if rootfsWriter != nil {
// Compress the qcow2 image if publishing a split image.
cmd = append(cmd, "-c")
}
reverter := revert.New()
defer reverter.Fail()
// Check for Direct I/O support.
from, err := os.OpenFile(mountInfo.DiskPath, unix.O_DIRECT|unix.O_RDONLY, 0)
if err == nil {
cmd = append(cmd, "-T", "none")
_ = from.Close()
}
to, err := os.OpenFile(fPath, unix.O_DIRECT|unix.O_CREAT, 0o600)
if err == nil {
cmd = append(cmd, "-t", "none")
_ = to.Close()
}
reverter.Add(func() { _ = os.Remove(fPath) })
cmd = append(cmd, mountInfo.DiskPath, fPath)
_, err = apparmor.QemuImg(d.state.OS, cmd, mountInfo.DiskPath, fPath, tracker)
if err != nil {
return nil, fmt.Errorf("Failed converting instance to qcow2: %w", err)
}
// Read converted file info and write file to tarball in the case of unified image
// For split images, just write as a qcow2 file
if rootfsWriter == nil {
imgOffset := len(tmpPath) + 1
fi, err = os.Lstat(fPath)
if err != nil {
return nil, err
}
err = metaTarWriter.WriteFile(fPath[imgOffset:], fPath, fi, false)
if err != nil {
return nil, err
}
} else {
f, err := os.Open(fPath)
if err != nil {
return nil, err
}
r := io.Reader(f)
_, err = util.SafeCopy(rootfsWriter, r)
if err != nil {
return nil, err
}
}
// Include all the templates.
fnam = d.TemplatesPath()
if util.PathExists(fnam) {
err = filepath.Walk(fnam, writeToMetaTar)
if err != nil {
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
}
err = metaTarWriter.Close()
if err != nil {
d.logger.Error("Failed exporting instance", ctxMap)
return nil, err
}
reverter.Success()
d.logger.Info("Exported instance", ctxMap)
return &meta, nil
}
// MigrateSend is not currently supported.
func (d *qemu) MigrateSend(args instance.MigrateSendArgs) error {
d.logger.Debug("Migration send starting")
defer d.logger.Debug("Migration send stopped")
// Check for stateful support.
if args.Live && !d.CanLiveMigrate() {
return errors.New("Live migration requires migration.stateful to be set to true")
}
// Setup a new operation.
op := operationlock.Get(d.Project().Name, d.Name())
if op != nil && op.ActionMatch(operationlock.ActionMigrate) {
return errors.New("The instance is already being migrated")
}
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionMigrate, nil, false, true)
if err != nil {
return err
}
// Wait for essential migration connections before negotiation.
connectionsCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
filesystemConn, err := args.FilesystemConn(connectionsCtx)
if err != nil {
op.Done(err)
return err
}
pool, err := storagePools.LoadByInstance(d.state, d)
if err != nil {
err := fmt.Errorf("Failed loading instance: %w", err)
op.Done(err)
return err
}
clusterMove := args.ClusterMoveSourceName != ""
remoteClusterMove := clusterMove && pool.Driver().Info().Remote
storageMove := args.StoragePool != ""
// The refresh argument passed to MigrationTypes() is always set
// to false here. The migration source/sender doesn't need to care whether
// or not it's doing a refresh as the migration sink/receiver will know
// this, and adjust the migration types accordingly.
// The same applies for clusterMove and storageMove, which are set to the most optimized defaults.
poolMigrationTypes := pool.MigrationTypes(storagePools.InstanceContentType(d), false, args.Snapshots, true, false)
if len(poolMigrationTypes) == 0 {
err := errors.New("No source migration types available")
op.Done(err)
return err
}
// Convert the pool's migration type options to an offer header to target.
// Populate the Fs, ZfsFeatures and RsyncFeatures fields.
offerHeader := localMigration.TypesToHeader(poolMigrationTypes...)
// Offer to send index header.
indexHeaderVersion := localMigration.IndexHeaderVersion
offerHeader.IndexHeaderVersion = &indexHeaderVersion
// For VMs, send block device size hint in offer header so that target can create the volume the same size.
blockSize, err := storagePools.InstanceDiskBlockSize(pool, d, d.op)
if err != nil {
err := fmt.Errorf("Failed getting source disk size: %w", err)
op.Done(err)
return err
}
d.logger.Debug("Set migration offer volume size", logger.Ctx{"blockSize": blockSize})
offerHeader.VolumeSize = &blockSize
srcConfig, err := pool.GenerateInstanceBackupConfig(d, args.Snapshots, true, d.op)
if err != nil {
err := fmt.Errorf("Failed generating instance migration config: %w", err)
op.Done(err)
return err
}
dependentVolumesOffer, err := storagePools.GenerateDependentVolumesOffer(d.state, srcConfig, d.Project().Name, args.Snapshots, args.Devices, args.ClusterMoveSourceName != "")
if err != nil {
err := fmt.Errorf("Failed generating instance depending volumes offer: %w", err)
op.Done(err)
return err
}
offerHeader.DependentVolumes = dependentVolumesOffer
contentType := storagePools.InstanceContentType(d)
// If we are copying snapshots, retrieve a list of snapshots from source volume.
if args.Snapshots {
offerHeader.SnapshotNames = make([]string, 0, len(srcConfig.Snapshots))
offerHeader.Snapshots = make([]*migration.Snapshot, 0, len(srcConfig.Snapshots))
for i := range srcConfig.Snapshots {
offerHeader.SnapshotNames = append(offerHeader.SnapshotNames, srcConfig.Snapshots[i].Name)
// Calculating snapshot size can be very slow, skip unless absolutely needed.
if !remoteClusterMove || storageMove {
snapSize, err := storagePools.CalculateVolumeSnapshotSize(d.Project().Name, pool, contentType, storageDrivers.VolumeTypeVM, d.Name(), srcConfig.Snapshots[i].Name)
if err != nil {
return err
}
srcConfig.Snapshots[i].Config["size"] = fmt.Sprintf("%d", snapSize)
}
offerHeader.Snapshots = append(offerHeader.Snapshots, instance.SnapshotToProtobuf(srcConfig.Snapshots[i]))
}
}
// Offer QEMU to QEMU live state transfer state transfer feature.
// If the request is for live migration, then offer that live QEMU to QEMU state transfer can proceed.
// Otherwise we'll fallback to doing stateful stop, migrate, and then stateful start, which will still
// fulfil the "live" part of the request, albeit with longer pause of the instance during the process.
if args.Live {
offerHeader.Criu = migration.CRIUType_VM_QEMU.Enum()
}
// Send offer to target.
d.logger.Debug("Sending migration offer to target")
err = args.ControlSend(offerHeader)
if err != nil {
err := fmt.Errorf("Failed sending migration offer header: %w", err)
op.Done(err)
return err
}
// Receive response from target.
d.logger.Debug("Waiting for migration offer response from target")
respHeader := &migration.MigrationHeader{}
err = args.ControlReceive(respHeader, true)
if err != nil {
err := fmt.Errorf("Failed receiving migration offer response: %w", err)
op.Done(err)
return err
}
d.logger.Debug("Got migration offer response from target")
// Negotiated migration types.
migrationTypes, err := localMigration.MatchTypes(respHeader, storagePools.FallbackMigrationType(contentType), poolMigrationTypes)
if err != nil {
err := fmt.Errorf("Failed to negotiate migration type: %w", err)
op.Done(err)
return err
}
volumesWithTypes, err := storagePools.DependentVolumesMatchMigrationType(d.state, respHeader.DependentVolumes, args.Snapshots, nil, true)
if err != nil {
err := fmt.Errorf("Failed to negotiate migration types for dependent volumes: %w", err)
op.Done(err)
return err
}
dependentVolumes := []localMigration.DependentVolumeArgs{}
for _, volWithType := range volumesWithTypes {
dependentVolumes = append(dependentVolumes, localMigration.ProtobufToDependentVolume(volWithType.Volume, volWithType.VolumeTypes[0], nil))
}
volSourceArgs := &localMigration.VolumeSourceArgs{
IndexHeaderVersion: respHeader.GetIndexHeaderVersion(), // Enable index header frame if supported.
Name: d.Name(),
MigrationType: migrationTypes[0],
Snapshots: offerHeader.SnapshotNames,
TrackProgress: true,
Refresh: respHeader.GetRefresh(),
AllowInconsistent: args.AllowInconsistent,
VolumeOnly: !args.Snapshots,
Info: &localMigration.Info{Config: srcConfig},
ClusterMove: clusterMove,
StorageMove: storageMove,
DependentVolumes: dependentVolumes,
}
// Only send the snapshots that the target requests when refreshing.
if respHeader.GetRefresh() {
volSourceArgs.Snapshots = respHeader.GetSnapshotNames()
allSnapshots := volSourceArgs.Info.Config.VolumeSnapshots
// Ensure that only the requested snapshots are included in the migration index header.
volSourceArgs.Info.Config.VolumeSnapshots = make([]*api.StorageVolumeSnapshot, 0, len(volSourceArgs.Snapshots))
for i := range allSnapshots {
if slices.Contains(volSourceArgs.Snapshots, allSnapshots[i].Name) {
volSourceArgs.Info.Config.VolumeSnapshots = append(volSourceArgs.Info.Config.VolumeSnapshots, allSnapshots[i])
}
}
}
// Detect whether the far side has chosen to use QEMU to QEMU live state transfer mode, and if so then
// wait for the connection to be established.
var stateConn io.ReadWriteCloser
if args.Live && respHeader.Criu != nil && *respHeader.Criu == migration.CRIUType_VM_QEMU {
stateConn, err = args.StateConn(connectionsCtx)
if err != nil {
op.Done(err)
return err
}
}
g, ctx := errgroup.WithContext(context.Background())
// Start control connection monitor.
g.Go(func() error {
d.logger.Debug("Migrate send control monitor started")
defer d.logger.Debug("Migrate send control monitor finished")
controlResult := make(chan error, 1) // Buffered to allow go routine to end if no readers.
// This will read the result message from the target side and detect disconnections.
go func() {
resp := migration.MigrationControl{}
err := args.ControlReceive(&resp, false)
if err != nil {
err = fmt.Errorf("Error reading migration control target: %w", err)
} else if !resp.GetSuccess() {
err = fmt.Errorf("Error from migration control target: %s", resp.GetMessage())
}
controlResult <- err
}()
// End as soon as we get control message/disconnection from the target side or a local error.
select {
case <-ctx.Done():
err = ctx.Err()
case err = <-controlResult:
}
return err
})
// Start error monitoring routine, this will detect when an error is returned from the other routines,
// and if that happens it will disconnect the migration connections which will trigger the other routines
// to finish.
go func() {
<-ctx.Done()
args.Disconnect()
}()
g.Go(func() error {
d.logger.Debug("Migrate send transfer started")
defer d.logger.Debug("Migrate send transfer finished")
var err error
// Start live state transfer using state connection if supported.
if stateConn != nil {
// When performing intra-cluster same-name move, take steps to prevent corruption
// of volatile device config keys during start & stop of instance on source/target.
if args.ClusterMoveSourceName == d.name {
// Disable VolatileSet from persisting changes to the database.
// This is so the volatile changes written by the running receiving member
// are not lost when the source instance is stopped.
d.volatileSetPersistDisable = true
// Store a reference to this instance (which has the old volatile settings)
// to allow the onStop hook to pick it up, which allows the devices being
// stopped to access their volatile settings stored when the instance
// originally started on this cluster member.
instanceRefSet(d)
defer instanceRefClear(d)
}
err = d.migrateSendLive(ctx, pool, args.ClusterMoveSourceName, args.StoragePool, blockSize, filesystemConn, stateConn, volSourceArgs)
if err != nil {
return err
}
} else {
// Perform stateful stop if live state transfer is not supported by target.
if args.Live {
err = d.Stop(true)
if err != nil {
return fmt.Errorf("Failed statefully stopping instance: %w", err)
}
}
err = pool.MigrateInstance(d, filesystemConn, volSourceArgs, d.op)
if err != nil {
return err
}
}
return nil
})
// Wait for routines to finish and collect first error.
{
err := g.Wait()
if err != nil {
op.Done(err)
return err
}
op.Done(nil)
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceMigrated.Event(d, nil))
return nil
}
}
// prepareEphemeralSnapshot sets up an overlay block device suitable for short lived operations.
// When backed is true, the overlay is opened with the disk's current top node as its backing
// node, as needed by copy-before-write overlays.
func (d *qemu) prepareEphemeralSnapshot(monitor *qmp.Monitor, diskName string, diskSize int64, backed bool) (string, string, func(), error) {
snapshotDiskName := ephemeralSnapshotName(diskName)
// Create snapshot of the disk.
// We use the VM's config volume for this so that the maximum size of the snapshot can be limited
// by setting the root disk's `size.state` property.
snapshotFile := filepath.Join(d.Path(), fmt.Sprintf("%s.qcow2", snapshotDiskName))
// Ensure there are no existing migration snapshot files.
err := os.Remove(snapshotFile)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return "", "", nil, err
}
// Create qcow2 disk image with the maximum size set to the instance's root disk size for use as
// a CoW target for the migration snapshot. This will be used during migration to store writes in
// the guest whilst the storage driver is transferring the root disk and snapshots to the target.
_, err = subprocess.RunCommand("qemu-img", "create", "-f", "qcow2", snapshotFile, fmt.Sprintf("%d", diskSize))
if err != nil {
return "", "", nil, fmt.Errorf("Failed opening file image for migration storage snapshot %q: %w", snapshotFile, err)
}
defer logger.WarnOnError(func() error { return os.Remove(snapshotFile) }, "Failed to remove snapshot file")
// Pass the snapshot file to the running QEMU process.
snapFile, err := os.OpenFile(snapshotFile, unix.O_RDWR, 0)
if err != nil {
return "", "", nil, fmt.Errorf("Failed opening file descriptor for migration storage snapshot %q: %w", snapshotFile, err)
}
defer logger.WarnOnError(snapFile.Close, "Failed to close snapshot file")
// Remove the snapshot file as we don't want to sync this to the target.
err = os.Remove(snapshotFile)
if err != nil {
return "", "", nil, err
}
info, err := monitor.SendFileWithFDSet(snapshotDiskName, snapFile, false)
if err != nil {
return "", "", nil, fmt.Errorf("Failed sending file descriptor of %q for migration storage snapshot: %w", snapFile.Name(), err)
}
defer logger.WarnOnError(func() error { return monitor.RemoveFDFromFDSet(snapshotDiskName) }, "Failed to remove FD from FD set")
_ = snapFile.Close() // Don't prevent clean unmount when instance is stopped.
// Find the disk's current top node, the base of the new overlay.
blockDevs, err := d.fetchBlockDeviceChain(monitor, diskName)
if err != nil {
return "", "", nil, fmt.Errorf("Failed fetching block device chain: %w", err)
}
blockDevName := blockDevs[len(blockDevs)-1]
blockDev := map[string]any{
"driver": "qcow2",
"node-name": snapshotDiskName,
"read-only": false,
"file": map[string]any{
"driver": "file",
"filename": fmt.Sprintf("/dev/fdset/%d", info.ID),
},
}
if backed {
blockDev["backing"] = blockDevName
}
// Add the snapshot file as a block device (not visible to the guest OS).
err = monitor.AddBlockDevice(blockDev, nil, false)
if err != nil {
return "", "", nil, fmt.Errorf("Failed adding migration storage snapshot block device: %w", err)
}
removeOverlay := func() {
err := monitor.RemoveBlockDevice(snapshotDiskName)
if err != nil {
d.logger.Error("Failed removing temporary snapshot disk device", logger.Ctx{"err": err})
}
}
return snapshotDiskName, blockDevName, removeOverlay, nil
}
// mergeEphemeralSnapshot merges an ephemeral snapshot back into its base disk and removes it.
// On merge failure the overlay is kept attached as it still holds the guest's writes.
func (d *qemu) mergeEphemeralSnapshot(monitor *qmp.Monitor, overlayNode string) error {
// Resume guest (this is needed as it will prevent merging the snapshot if paused).
err := monitor.Start()
if err != nil {
d.logger.Warn("Failed resuming instance", logger.Ctx{"err": err})
}
// Merge the snapshot back into the source disk so we don't lose writes,
// retrying as failures can be transient.
for i := range 3 {
if i > 0 {
time.Sleep(time.Second)
}
err = monitor.BlockCommit(overlayNode, "", "")
if err == nil {
break
}
}
if err != nil {
// Keep the overlay attached, removing it would discard the guest's writes.
d.logger.Error("Failed merging temporary storage snapshot, guest writes remain in the overlay", logger.Ctx{"overlay": overlayNode, "err": err})
return fmt.Errorf("Failed merging temporary storage snapshot %q: %w", overlayNode, err)
}
err = monitor.RemoveBlockDevice(overlayNode)
if err != nil {
d.logger.Error("Failed removing temporary snapshot disk device", logger.Ctx{"err": err})
return err
}
return nil
}
// removeEphemeralOverlay tears down a copy-before-write overlay, cancelling its backup job first.
func (d *qemu) removeEphemeralOverlay(monitor *qmp.Monitor, overlayNode string) error {
// Cancel the copy-before-write job if it is still running.
err := monitor.BlockJobCancelWait(overlayNode)
if err != nil {
d.logger.Debug("Failed cancelling overlay block job", logger.Ctx{"overlay": overlayNode, "err": err})
}
err = monitor.RemoveBlockDevice(overlayNode)
if err != nil {
return fmt.Errorf("Failed removing temporary snapshot overlay %q: %w", overlayNode, err)
}
return nil
}
// createEphemeralSnapshot creates a temporary snapshot of the disk that is intended for short-lived operations.
func (d *qemu) createEphemeralSnapshot(diskName string, diskSize int64) (func(), error) {
monitor, err := d.qmpConnect()
if err != nil {
return nil, err
}
snapshotDiskName, blockDevName, removeOverlay, err := d.prepareEphemeralSnapshot(monitor, diskName, diskSize, false)
if err != nil {
return nil, err
}
// Take a snapshot of the disk and redirect writes to the snapshot disk.
err = monitor.BlockDevSnapshot(blockDevName, snapshotDiskName)
if err != nil {
removeOverlay()
return nil, fmt.Errorf("Failed taking temporary migration storage snapshot: %w", err)
}
cleanup := func() {
_ = d.mergeEphemeralSnapshot(monitor, snapshotDiskName)
}
return cleanup, nil
}
// sendMigrationSnapshot transfers the snapshot to the target.
// If finalize is true, it performs cleanup after migration.
// Otherwise, it returns a finalize function that can be called later.
func (d *qemu) sendMigrationSnapshot(diskName string, filesystemConn io.ReadWriteCloser, finalize bool) (func() error, error) {
monitor, err := d.qmpConnect()
if err != nil {
return nil, err
}
reverter := revert.New()
defer reverter.Fail()
targetDiskName := migrationNBDTarget(diskName)
snapshotDiskName := ephemeralSnapshotName(diskName)
listener, err := net.Listen("unix", "")
if err != nil {
return nil, fmt.Errorf("Failed creating NBD unix listener: %w", err)
}
defer logger.WarnOnError(listener.Close, "Failed to close listener")
g, _ := errgroup.WithContext(context.Background())
g.Go(func() error {
d.logger.Debug("NBD listener waiting for accept")
nbdConn, err := listener.Accept()
if err != nil {
return fmt.Errorf("Failed accepting connection to NBD client unix listener: %w", err)
}
defer logger.WarnOnError(nbdConn.Close, "Failed to close connection")
d.logger.Debug("NBD connection on source started")
go func() { _, _ = util.SafeCopy(filesystemConn, nbdConn) }()
_, _ = util.SafeCopy(nbdConn, filesystemConn)
d.logger.Debug("NBD connection on source finished")
return nil
})
// Connect to NBD migration target and add it the source instance as a disk device.
d.logger.Debug("Connecting to migration NBD storage target", logger.Ctx{"targetDiskName": targetDiskName, "diskName": diskName})
err = monitor.AddBlockDevice(map[string]any{
"node-name": targetDiskName,
"driver": "raw",
"file": map[string]any{
"driver": "nbd",
"export": diskName,
"server": map[string]any{
"type": "unix",
"abstract": true,
"path": strings.TrimPrefix(listener.Addr().String(), "@"),
},
},
}, nil, false)
if err != nil {
return nil, fmt.Errorf("Failed adding NBD device: %w", err)
}
reverter.Add(func() {
time.Sleep(time.Second) // Wait for it to be released.
err := monitor.RemoveBlockDevice(targetDiskName)
if err != nil {
d.logger.Warn("Failed removing NBD storage target device", logger.Ctx{"err": err})
}
})
d.logger.Debug("Connected to migration NBD storage target")
// Begin transferring any writes that occurred during the storage migration by transferring the
// contents of the (top) migration snapshot to the target disk to bring them into sync.
// Once this has completed the guest OS will be paused.
d.logger.Debug("Migration storage snapshot transfer started")
err = monitor.BlockDevMirror(snapshotDiskName, targetDiskName)
if err != nil {
return nil, fmt.Errorf("Failed transferring migration storage snapshot: %w", err)
}
reverter.Add(func() {
err = monitor.BlockJobCancel(snapshotDiskName)
if err != nil {
d.logger.Error("Failed cancelling block job", logger.Ctx{"err": err})
}
})
d.logger.Debug("Migration storage snapshot transfer finished")
finalizeFunc := func() error {
filesystemConn.Close()
_ = g.Wait()
// Complete the migration snapshot sync process (the guest OS will remain paused).
d.logger.Debug("Migration storage snapshot transfer commit started")
err = monitor.BlockJobCancel(snapshotDiskName)
if err != nil {
return fmt.Errorf("Failed cancelling block job: %w", err)
}
d.logger.Debug("Migration storage snapshot transfer commit finished")
time.Sleep(time.Second) // Wait for it to be released.
// Remove the NBD client disk.
err = monitor.RemoveBlockDevice(targetDiskName)
if err != nil {
d.logger.Warn("Failed removing NBD storage target device", logger.Ctx{"err": err})
}
d.logger.Debug("Removed NBD storage target device")
// Merge snapshot back to the source disk so we don't lose the writes.
err = monitor.BlockCommit(snapshotDiskName, "", "")
if err != nil {
return fmt.Errorf("Failed merging migration storage snapshot: %w", err)
}
err = monitor.RemoveBlockDevice(snapshotDiskName)
if err != nil {
return fmt.Errorf("Failed removing temporary snapshot disk device: %w", err)
}
return nil
}
if finalize {
err = finalizeFunc()
if err != nil {
return nil, err
}
}
reverter.Success()
return finalizeFunc, nil
}
// migrateSendLive performs live migration send process.
func (d *qemu) migrateSendLive(ctx context.Context, pool storagePools.Pool, clusterMoveSourceName string, storagePool string, rootDiskSize int64, filesystemConn io.ReadWriteCloser, stateConn io.ReadWriteCloser, volSourceArgs *localMigration.VolumeSourceArgs) error {
monitor, err := d.qmpConnect()
if err != nil {
return err
}
// Get the root disk device config.
rootDiskName, _, err := d.getRootDiskDevice()
if err != nil {
return err
}
rootDiskName = d.blockNodeName(linux.PathNameEncode(rootDiskName))
// If we are performing an intra-cluster member move on a Ceph storage pool without storage change
// then we can treat this as shared storage and avoid needing to sync the root disk.
sameSharedStorage := clusterMoveSourceName != "" && pool.Driver().Info().Remote && storagePool == ""
disksToMigrate := len(volSourceArgs.DependentVolumes) > 0
dependentVolumeMove := clusterMoveSourceName != "" && disksToMigrate
reverter := revert.New()
// Non-shared storage snapshot setup.
if !sameSharedStorage || dependentVolumeMove {
// Setup migration capabilities.
capabilities := map[string]bool{
// Automatically throttle down the guest to speed up convergence of RAM migration.
"auto-converge": true,
// Allow the migration to be paused after the source qemu releases the block devices but
// before the serialisation of the device state, to avoid a race condition between
// migration and blockdev-mirror. This requires that the migration be continued after it
// has reached the "pre-switchover" status.
"pause-before-switchover": true,
}
err = monitor.MigrateSetCapabilities(capabilities)
if err != nil {
return fmt.Errorf("Failed setting migration capabilities: %w", err)
}
parameters := map[string]any{
"cpu-throttle-initial": 50,
"throttle-trigger-threshold": 20,
}
err = monitor.MigrateSetParameters(parameters)
if err != nil {
return fmt.Errorf("Failed setting migration parameters: %w", err)
}
if !sameSharedStorage {
cleanup, err := d.createEphemeralSnapshot(rootDiskName, rootDiskSize)
if err != nil {
return fmt.Errorf("Failed creating migration snapshot: %w", err)
}
reverter.Add(cleanup)
}
for _, vol := range volSourceArgs.DependentVolumes {
diskName := d.blockNodeName(linux.PathNameEncode(vol.DeviceName))
d.logger.Debug("Create snapshot for dependent volume", logger.Ctx{"name": vol.Name, "size": vol.VolumeSize, "diskName": diskName})
cleanup, err := d.createEphemeralSnapshot(diskName, vol.VolumeSize)
if err != nil {
return fmt.Errorf("Failed creating migration snapshot: %w", err)
}
reverter.Add(cleanup)
}
d.logger.Debug("Setup temporary migration storage snapshot")
} else {
// Still set some options for shared storage.
capabilities := map[string]bool{
// Automatically throttle down the guest to speed up convergence of RAM migration.
"auto-converge": true,
}
err = monitor.MigrateSetCapabilities(capabilities)
if err != nil {
return fmt.Errorf("Failed setting migration capabilities: %w", err)
}
parameters := map[string]any{
"cpu-throttle-initial": 50,
"throttle-trigger-threshold": 20,
}
err = monitor.MigrateSetParameters(parameters)
if err != nil {
return fmt.Errorf("Failed setting migration parameters: %w", err)
}
}
// Perform storage transfer while instance is still running.
// For shared storage the storage driver will likely not do much here, but we still call it anyway for the
// sense checks it performs.
// We enable AllowInconsistent mode as this allows for transferring the VM storage whilst it is running
// and the snapshot we took earlier is designed to provide consistency anyway.
volSourceArgs.AllowInconsistent = true
err = pool.MigrateInstance(d, filesystemConn, volSourceArgs, d.op)
if err != nil {
return err
}
// Derive the effective storage project name from the instance config's project.
storageProjectName, err := project.StorageVolumeProject(d.state.DB.Cluster, d.project.Name, db.StoragePoolVolumeTypeCustom)
if err != nil {
return err
}
// Notify the shared disks that they're going to be accessed from another system,
// but only when performing a move within the same storage pool.
if storagePool == "" && clusterMoveSourceName != "" {
for _, dev := range d.expandedDevices.Sorted() {
if dev.Config["type"] != "disk" || dev.Config["path"] == "/" || dev.Config["pool"] == "" {
continue
}
// Load the pool for the disk.
diskPool, err := storagePools.LoadByName(d.state, dev.Config["pool"])
if err != nil {
return fmt.Errorf("Failed loading storage pool: %w", err)
}
// Check that we're on shared storage.
if !diskPool.Driver().Info().Remote {
continue
}
// Setup the volume entry.
extraSourceArgs := &localMigration.VolumeSourceArgs{
ClusterMove: true,
}
vol := diskPool.GetVolume(storageDrivers.VolumeTypeCustom, storageDrivers.ContentTypeBlock, project.StorageVolume(storageProjectName, dev.Config["source"]), nil)
// Call MigrateVolume on the source.
err = diskPool.Driver().MigrateVolume(vol, nil, extraSourceArgs, nil)
if err != nil {
return fmt.Errorf("Failed to prepare device %q for migration: %w", dev.Name, err)
}
}
}
var finalizeRootTransfer func() error
if !sameSharedStorage {
finalizeRootTransfer, err = d.sendMigrationSnapshot(rootDiskName, filesystemConn, false)
if err != nil {
return fmt.Errorf("Failed transferring snapshot disk: %w", err)
}
}
d.logger.Debug("Stateful migration checkpoint send starting")
// Send checkpoint to QEMU process on target. This will pause the guest OS (if not already paused).
pipeRead, pipeWrite, err := os.Pipe()
if err != nil {
return err
}
defer func() {
_ = pipeRead.Close()
_ = pipeWrite.Close()
}()
go func() { _, _ = util.SafeCopy(stateConn, pipeRead) }()
err = d.saveStateHandle(monitor, pipeWrite)
if err != nil {
return fmt.Errorf("Failed starting state transfer to target: %w", err)
}
// Start monitoring the migration progress.
chMonitor := make(chan bool, 1)
if d.op != nil {
go func() {
for {
// Wait for next update.
select {
case <-chMonitor:
return
case <-time.After(time.Second):
}
// Get current migration progress.
progress, err := monitor.QueryMigrate()
if err != nil {
// Stop monitoring on error.
return
}
// Post update.
percent := int64(float64(progress.RAM.Transferred) / float64(progress.RAM.Total) * float64(100))
speed := int64(progress.RAM.MBps * 1024 * 1024 / 8)
metadata := map[string]any{}
metadata["progress"] = map[string]string{
"stage": "live_migrate_instance",
"processed": strconv.FormatInt(progress.RAM.Transferred, 10),
"percent": strconv.FormatInt(percent, 10),
"speed": strconv.FormatInt(speed, 10),
}
metadata["live_migrate_instance_progress"] = fmt.Sprintf("Live migration: %s remaining (%s/s) (%d%% CPU throttle)", units.GetByteSizeString(progress.RAM.Remaining, 2), units.GetByteSizeString(speed, 2), progress.CPUThrottlePercentage)
_ = d.op.UpdateMetadata(metadata)
}
}()
}
// Non-shared storage snapshot transfer finalization.
if !sameSharedStorage || dependentVolumeMove {
// Wait until state transfer has reached pre-switchover state (the guest OS will remain paused).
err = monitor.MigrateWait(ctx, "pre-switchover")
if err != nil {
return fmt.Errorf("Failed waiting for state transfer to reach pre-switchover stage: %w", err)
}
d.logger.Debug("Stateful migration checkpoint reached pre-switchover phase")
if finalizeRootTransfer != nil {
err = finalizeRootTransfer()
if err != nil {
return fmt.Errorf("Failed transferring root snapshot disk: %w", err)
}
}
for _, vol := range volSourceArgs.DependentVolumes {
diskName := d.blockNodeName(linux.PathNameEncode(vol.DeviceName))
_, err = d.sendMigrationSnapshot(diskName, filesystemConn, true)
if err != nil {
return fmt.Errorf("Failed transferring snapshot disk: %w", err)
}
}
// Finalise the migration state transfer (the guest OS will remain paused).
err = monitor.MigrateContinue("pre-switchover")
if err != nil {
return fmt.Errorf("Failed continuing state transfer: %w", err)
}
d.logger.Debug("Stateful migration checkpoint send continuing")
}
// Wait until the migration state transfer has completed (the guest OS will remain paused).
err = monitor.MigrateWait(ctx, "completed")
if err != nil {
return fmt.Errorf("Failed waiting for state transfer to reach completed stage: %w", err)
}
close(chMonitor)
d.logger.Debug("Stateful migration checkpoint send finished")
if clusterMoveSourceName != "" {
// If doing an intra-cluster member move then we will be deleting the instance on the source,
// so lets just stop it after migration is completed.
err = d.Stop(false)
if err != nil {
return fmt.Errorf("Failed stopping instance: %w", err)
}
} else {
// Resume guest.
err = monitor.Start()
if err != nil {
return fmt.Errorf("Failed resuming instance: %w", err)
}
d.logger.Debug("Resumed instance")
}
reverter.Success()
return nil
}
// MigrateReceive receives an instance being migrated from a source.
func (d *qemu) MigrateReceive(args instance.MigrateReceiveArgs) error {
d.logger.Debug("Migration receive starting")
defer d.logger.Debug("Migration receive stopped")
// Wait for essential migration connections before negotiation.
connectionsCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
filesystemConn, err := args.FilesystemConn(connectionsCtx)
if err != nil {
return err
}
// Receive offer from source.
d.logger.Debug("Waiting for migration offer from source")
offerHeader := &migration.MigrationHeader{}
err = args.ControlReceive(offerHeader, true)
if err != nil {
return fmt.Errorf("Failed receiving migration offer from source: %w", err)
}
// When doing a cluster same-name move we cannot load the storage pool using the instance's volume DB
// record because it may be associated to the wrong cluster member. Instead we ascertain the pool to load
// using the instance's root disk device.
if args.ClusterMoveSourceName == d.name {
if args.StoragePool != "" {
d.storagePool, err = storagePools.LoadByName(d.state, args.StoragePool)
if err != nil {
return fmt.Errorf("Failed loading storage pool: %w", err)
}
} else {
_, rootDiskDevice, err := d.getRootDiskDevice()
if err != nil {
return fmt.Errorf("Failed getting root disk: %w", err)
}
if rootDiskDevice["pool"] == "" {
return errors.New("The instance's root device is missing the pool property")
}
// Initialize the storage pool cache.
d.storagePool, err = storagePools.LoadByName(d.state, rootDiskDevice["pool"])
if err != nil {
return fmt.Errorf("Failed loading storage pool: %w", err)
}
}
}
pool, err := storagePools.LoadByInstance(d.state, d)
if err != nil {
return err
}
// The source will never set Refresh in the offer header.
// However, to determine the correct migration type Refresh needs to be set.
offerHeader.Refresh = &args.Refresh
clusterMove := args.ClusterMoveSourceName != ""
storageMove := args.StoragePool != ""
// Extract the source's migration type and then match it against our pool's supported types and features.
// If a match is found the combined features list will be sent back to requester.
contentType := storagePools.InstanceContentType(d)
respTypes, err := localMigration.MatchTypes(offerHeader, storagePools.FallbackMigrationType(contentType), pool.MigrationTypes(contentType, args.Refresh, args.Snapshots, clusterMove, storageMove))
if err != nil {
return err
}
// The migration header to be sent back to source with our target options.
// Convert response type to response header and copy snapshot info into it.
respHeader := localMigration.TypesToHeader(respTypes...)
// Respond with our maximum supported header version if the requested version is higher than ours.
// Otherwise just return the requested header version to the source.
indexHeaderVersion := min(offerHeader.GetIndexHeaderVersion(), localMigration.IndexHeaderVersion)
respHeader.IndexHeaderVersion = &indexHeaderVersion
respHeader.SnapshotNames = offerHeader.SnapshotNames
respHeader.Snapshots = offerHeader.Snapshots
respHeader.Refresh = &args.Refresh
localDevices := d.localDevices.CloneNative()
volumesWithTypes, err := storagePools.DependentVolumesMatchMigrationType(d.state, offerHeader.DependentVolumes, args.Snapshots, localDevices, false)
if err != nil {
return fmt.Errorf("Failed to negotiate migration types for dependent volumes: %w", err)
}
dependentVolumes := []localMigration.DependentVolumeArgs{}
for _, volWithType := range volumesWithTypes {
respHeader.DependentVolumes = append(respHeader.DependentVolumes, volWithType.Volume)
vol := localMigration.ProtobufToDependentVolume(volWithType.Volume, volWithType.VolumeTypes[0], localDevices[*volWithType.Volume.DeviceName])
dependentVolumes = append(dependentVolumes, vol)
}
if args.Refresh {
// Get the remote snapshots on the source.
sourceSnapshots := offerHeader.GetSnapshots()
sourceSnapshotComparable := make([]storagePools.ComparableSnapshot, 0, len(sourceSnapshots))
for _, sourceSnap := range sourceSnapshots {
sourceSnapshotComparable = append(sourceSnapshotComparable, storagePools.ComparableSnapshot{
Name: sourceSnap.GetName(),
CreationDate: time.Unix(sourceSnap.GetCreationDate(), 0),
})
}
// Get existing snapshots on the local target.
targetSnapshots, err := d.Snapshots()
if err != nil {
return err
}
targetSnapshotsComparable := make([]storagePools.ComparableSnapshot, 0, len(targetSnapshots))
for _, targetSnap := range targetSnapshots {
_, targetSnapName, _ := api.GetParentAndSnapshotName(targetSnap.Name())
targetSnapshotsComparable = append(targetSnapshotsComparable, storagePools.ComparableSnapshot{
Name: targetSnapName,
CreationDate: targetSnap.CreationDate(),
})
}
// Compare the two sets.
syncSourceSnapshotIndexes, deleteTargetSnapshotIndexes := storagePools.CompareSnapshots(sourceSnapshotComparable, targetSnapshotsComparable, args.RefreshExcludeOlder)
// Delete the extra local snapshots first.
for _, deleteTargetSnapshotIndex := range deleteTargetSnapshotIndexes {
err := targetSnapshots[deleteTargetSnapshotIndex].Delete(true, true)
if err != nil {
return err
}
}
// Only request to send the snapshots that need updating.
syncSnapshotNames := make([]string, 0, len(syncSourceSnapshotIndexes))
syncSnapshots := make([]*migration.Snapshot, 0, len(syncSourceSnapshotIndexes))
for _, syncSourceSnapshotIndex := range syncSourceSnapshotIndexes {
syncSnapshotNames = append(syncSnapshotNames, sourceSnapshots[syncSourceSnapshotIndex].GetName())
syncSnapshots = append(syncSnapshots, sourceSnapshots[syncSourceSnapshotIndex])
}
respHeader.Snapshots = syncSnapshots
respHeader.SnapshotNames = syncSnapshotNames
offerHeader.Snapshots = syncSnapshots
offerHeader.SnapshotNames = syncSnapshotNames
}
// Negotiate support for QEMU to QEMU live state transfer.
// If the request is for live migration, then respond that live QEMU to QEMU state transfer can proceed.
// Otherwise we'll fallback to doing stateful stop, migrate, and then stateful start, which will still
// fulfil the "live" part of the request, albeit with longer pause of the instance during the process.
poolInfo := pool.Driver().Info()
var useStateConn bool
if args.Live && offerHeader.Criu != nil && *offerHeader.Criu == migration.CRIUType_VM_QEMU {
respHeader.Criu = migration.CRIUType_VM_QEMU.Enum()
useStateConn = true
}
// Send response to source.
d.logger.Debug("Sending migration response to source")
err = args.ControlSend(respHeader)
if err != nil {
return fmt.Errorf("Failed sending migration response to source: %w", err)
}
d.logger.Debug("Sent migration response to source")
// Establish state transfer connection if needed.
var stateConn io.ReadWriteCloser
if args.Live && useStateConn {
stateConn, err = args.StateConn(connectionsCtx)
if err != nil {
return err
}
}
reverter := revert.New()
defer reverter.Fail()
g, ctx := errgroup.WithContext(context.Background())
// Start control connection monitor.
g.Go(func() error {
d.logger.Debug("Migrate receive control monitor started")
defer d.logger.Debug("Migrate receive control monitor finished")
controlResult := make(chan error, 1) // Buffered to allow go routine to end if no readers.
// This will read the result message from the source side and detect disconnections.
go func() {
resp := migration.MigrationControl{}
err := args.ControlReceive(&resp, false)
if err != nil {
err = fmt.Errorf("Error reading migration control source: %w", err)
} else if !resp.GetSuccess() {
err = fmt.Errorf("Error from migration control source: %s", resp.GetMessage())
}
controlResult <- err
}()
// End as soon as we get control message/disconnection from the source side or a local error.
select {
case <-ctx.Done():
err = ctx.Err()
case err = <-controlResult:
}
return err
})
// Start error monitoring routine, this will detect when an error is returned from the other routines,
// and if that happens it will disconnect the migration connections which will trigger the other routines
// to finish.
go func() {
<-ctx.Done()
args.Disconnect()
}()
// Start filesystem transfer routine and initialize a channel that is closed when the routine finishes.
fsTransferDone := make(chan struct{})
g.Go(func() error {
defer close(fsTransferDone)
d.logger.Debug("Migrate receive transfer started")
defer d.logger.Debug("Migrate receive transfer finished")
var err error
snapshots := make([]*migration.Snapshot, 0)
// Legacy: we only sent the snapshot names, so we just copy the instances's config over,
// same as we used to do.
if len(offerHeader.SnapshotNames) != len(offerHeader.Snapshots) {
// Convert the instance to an api.InstanceSnapshot.
profileNames := make([]string, 0, len(d.Profiles()))
for _, p := range d.Profiles() {
profileNames = append(profileNames, p.Name)
}
architectureName, _ := osarch.ArchitectureName(d.Architecture())
apiInstSnap := &api.InstanceSnapshot{
InstanceSnapshotPut: api.InstanceSnapshotPut{
ExpiresAt: time.Time{},
},
Architecture: architectureName,
CreatedAt: d.CreationDate(),
LastUsedAt: d.LastUsedDate(),
Config: d.LocalConfig(),
Description: d.Description(),
Devices: d.LocalDevices().CloneNative(),
Ephemeral: d.IsEphemeral(),
Stateful: d.IsStateful(),
Profiles: profileNames,
}
for _, name := range offerHeader.SnapshotNames {
base := instance.SnapshotToProtobuf(apiInstSnap)
base.Name = &name
snapshots = append(snapshots, base)
}
} else {
snapshots = offerHeader.Snapshots
}
volTargetArgs := localMigration.VolumeTargetArgs{
IndexHeaderVersion: respHeader.GetIndexHeaderVersion(),
Name: d.Name(),
MigrationType: respTypes[0],
Refresh: args.Refresh, // Indicate to receiver volume should exist.
TrackProgress: true, // Use a progress tracker on receiver to get in-cluster progress information.
Live: args.Live,
VolumeSize: offerHeader.GetVolumeSize(), // Block size setting override.
VolumeOnly: !args.Snapshots,
ClusterMoveSourceName: args.ClusterMoveSourceName,
StoragePool: args.StoragePool,
DependentVolumes: dependentVolumes,
}
// At this point we have already figured out the parent instances's root
// disk device so we can simply retrieve it from the expanded devices.
parentStoragePool := ""
parentExpandedDevices := d.ExpandedDevices()
parentLocalRootDiskDeviceKey, parentLocalRootDiskDevice, _ := internalInstance.GetRootDiskDevice(parentExpandedDevices.CloneNative())
if parentLocalRootDiskDeviceKey != "" {
parentStoragePool = parentLocalRootDiskDevice["pool"]
}
if parentStoragePool == "" {
return errors.New("Instance's root device is missing the pool property")
}
// A zero length Snapshots slice indicates volume only migration in
// VolumeTargetArgs. So if VolumeOnly was requested, do not populate them.
if args.Snapshots {
volTargetArgs.Snapshots = make([]*migration.Snapshot, 0, len(snapshots))
for _, snap := range snapshots {
migrationSnapshot := &migration.Snapshot{Name: snap.Name}
migration.SetSnapshotConfigValue(migrationSnapshot, "size", migration.GetSnapshotConfigValue(snap, "size"))
volTargetArgs.Snapshots = append(volTargetArgs.Snapshots, migrationSnapshot)
// Only create snapshot instance DB records if not doing a cluster same-name move.
// As otherwise the DB records will already exist.
if args.ClusterMoveSourceName != d.name {
snapArgs, err := instance.SnapshotProtobufToInstanceArgs(d.state, d, snap)
if err != nil {
return err
}
// The offerHeader, depending on the case, stores information about either an InstanceSnapshot
// or a StorageVolumeSnapshot. In the Config, we pass information about the volume size,
// but an InstanceSnapshot config cannot have a 'size' key. This key should be removed
// before passing the data to the CreateInternal method.
delete(snapArgs.Config, "size")
// Ensure that snapshot and parent instance have the same storage pool in
// their local root disk device. If the root disk device for the snapshot
// comes from a profile on the new instance as well we don't need to do
// anything.
if snapArgs.Devices != nil {
snapLocalRootDiskDeviceKey, _, _ := internalInstance.GetRootDiskDevice(snapArgs.Devices.CloneNative())
if snapLocalRootDiskDeviceKey != "" {
snapArgs.Devices[snapLocalRootDiskDeviceKey]["pool"] = parentStoragePool
}
}
// Create the snapshot instance.
_, snapInstOp, cleanup, err := instance.CreateInternal(d.state, *snapArgs, d.op, true, false, false)
if err != nil {
return fmt.Errorf("Failed creating instance snapshot record %q: %w", snapArgs.Name, err)
}
reverter.Add(cleanup)
defer snapInstOp.Done(err)
}
}
}
err = pool.CreateInstanceFromMigration(d, filesystemConn, volTargetArgs, d.op)
if err != nil {
return fmt.Errorf("Failed creating instance on target: %w", err)
}
isRemoteClusterMove := clusterMove && poolInfo.Remote
reverter.Add(func() {
// Delete the instance unless it is moved within the same cluster on a shared pool.
if (!isRemoteClusterMove && !storageMove) || storageMove {
_ = pool.DeleteInstance(d, d.op)
}
})
// Derive the effective storage project name from the instance config's project.
storageProjectName, err := project.StorageVolumeProject(d.state.DB.Cluster, d.project.Name, db.StoragePoolVolumeTypeCustom)
if err != nil {
return err
}
// Notify the shared disks that they're going to be accessed from another system,
// but only when performing a move within the same storage pool.
if !storageMove && args.ClusterMoveSourceName != "" {
for _, dev := range d.expandedDevices.Sorted() {
if dev.Config["type"] != "disk" || dev.Config["path"] == "/" || dev.Config["pool"] == "" {
continue
}
// Load the pool for the disk.
diskPool, err := storagePools.LoadByName(d.state, dev.Config["pool"])
if err != nil {
return fmt.Errorf("Failed loading storage pool: %w", err)
}
// Check that we're on shared storage.
if !diskPool.Driver().Info().Remote {
continue
}
// Setup the volume entry.
extraTargetArgs := localMigration.VolumeTargetArgs{
ClusterMoveSourceName: args.ClusterMoveSourceName,
StoragePool: args.StoragePool,
}
vol := diskPool.GetVolume(storageDrivers.VolumeTypeCustom, storageDrivers.ContentTypeBlock, project.StorageVolume(storageProjectName, dev.Config["source"]), nil)
// Call CreateVolumeFromMigration on the target.
err = diskPool.Driver().CreateVolumeFromMigration(vol, nil, extraTargetArgs, nil, nil)
if err != nil {
return fmt.Errorf("Failed to prepare device %q for migration: %w", dev.Name, err)
}
}
}
// Only delete all instance volumes on error if the pool volume creation has succeeded to
// avoid deleting an existing conflicting volume.
if !volTargetArgs.Refresh && !isRemoteClusterMove {
reverter.Add(func() {
snapshots, _ := d.Snapshots()
snapshotCount := len(snapshots)
for k := range snapshots {
// Delete the snapshots in reverse order.
k = snapshotCount - 1 - k
_ = pool.DeleteInstanceSnapshot(snapshots[k], nil)
}
_ = pool.DeleteInstance(d, nil)
})
}
if args.ClusterMoveSourceName != d.name {
err = d.DeferTemplateApply(instance.TemplateTriggerCopy)
if err != nil {
return err
}
}
if args.Live {
// Start live state transfer using state connection if supported.
if stateConn != nil {
d.migrationReceiveStateful = map[string]io.ReadWriteCloser{
api.SecretNameState: stateConn,
}
d.disksToMigrate = append(d.disksToMigrate, dependentVolumes...)
dependentVolumeMove := args.ClusterMoveSourceName != "" && len(d.disksToMigrate) > 0
// Populate the filesystem connection handle if doing non-shared storage migration.
sameSharedStorage := args.ClusterMoveSourceName != "" && poolInfo.Remote && args.StoragePool == ""
if !sameSharedStorage || dependentVolumeMove {
d.migrationReceiveStateful[api.SecretNameFilesystem] = filesystemConn
}
d.migrationRootDisk = !sameSharedStorage
d.migrationClusterMove = args.ClusterMoveSourceName != ""
}
// Although the instance technically isn't considered stateful, we set this to allow
// starting from the migrated state file or migration state connection.
d.stateful = true
err = d.start(true, args.InstanceOperation)
if err != nil {
return err
}
}
return nil
})
{
// Wait until the filesystem transfer routine has finished.
<-fsTransferDone
// If context is cancelled by this stage, then an error has occurred.
// Wait for all routines to finish and collect the first error that occurred.
if ctx.Err() != nil {
err := g.Wait()
// Send failure response to source.
msg := migration.MigrationControl{
Success: proto.Bool(err == nil),
}
if err != nil {
msg.Message = proto.String(err.Error())
}
d.logger.Debug("Sending migration failure response to source", logger.Ctx{"err": err})
sendErr := args.ControlSend(&msg)
if sendErr != nil {
d.logger.Warn("Failed sending migration failure to source", logger.Ctx{"err": sendErr})
}
return err
}
// Send success response to source to control as nothing has gone wrong so far.
msg := migration.MigrationControl{
Success: proto.Bool(true),
}
d.logger.Debug("Sending migration success response to source", logger.Ctx{"success": msg.GetSuccess()})
err := args.ControlSend(&msg)
if err != nil {
d.logger.Warn("Failed sending migration success to source", logger.Ctx{"err": err})
return fmt.Errorf("Failed sending migration success to source: %w", err)
}
// Wait for all routines to finish (in this case it will be the control monitor) but do
// not collect the error, as it will just be a disconnect error from the source.
_ = g.Wait()
reverter.Success()
return nil
}
}
// CGroup is not implemented for VMs.
func (d *qemu) CGroup() (*cgroup.CGroup, error) {
return nil, instance.ErrNotImplemented
}
// FileSFTPConn returns a connection to the agent SFTP endpoint.
func (d *qemu) FileSFTPConn() (net.Conn, error) {
// VMs, unlike containers, cannot perform file operations if not running and using the agent.
if !d.IsRunning() {
return nil, errors.New("Instance is not running")
}
// Connect to the agent.
client, err := d.getAgentClient()
if err != nil {
return nil, err
}
// Get the HTTP transport.
httpTransport, ok := client.Transport.(*http.Transport)
if !ok {
return nil, errors.New("Agent client transport is not an *http.Transport")
}
// Send the upgrade request.
u, err := url.Parse("https://custom.socket/1.0/sftp")
if err != nil {
return nil, err
}
req := &http.Request{
Method: http.MethodGet,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Host: u.Host,
}
req.Header["Upgrade"] = []string{"sftp"}
req.Header["Connection"] = []string{"Upgrade"}
conn, err := httpTransport.DialContext(context.Background(), "tcp", "8443")
if err != nil {
return nil, err
}
tlsConn := tls.Client(conn, httpTransport.TLSClientConfig)
err = tlsConn.Handshake()
if err != nil {
return nil, err
}
err = req.Write(tlsConn)
if err != nil {
return nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(tlsConn), req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusSwitchingProtocols {
return nil, fmt.Errorf("Dialing failed: expected status code 101 got %d", resp.StatusCode)
}
if resp.Header.Get("Upgrade") != "sftp" {
return nil, errors.New("Missing or unexpected Upgrade header in response")
}
return tlsConn, nil
}
// FileSFTP returns an SFTP connection to the agent endpoint.
func (d *qemu) FileSFTP() (*sftp.Client, error) {
// Connect to the forkfile daemon.
conn, err := d.FileSFTPConn()
if err != nil {
return nil, err
}
// Get a SFTP client.
client, err := sftp.NewClientPipe(conn, conn)
if err != nil {
_ = conn.Close()
return nil, err
}
go func() {
// Wait for the client to be done before closing the connection.
_ = client.Wait()
_ = conn.Close()
}()
return client, nil
}
// PortForwardConn connects to the given address and TCP port inside of the instance through the agent.
func (d *qemu) PortForwardConn(address string, port int) (net.Conn, error) {
// VMs, unlike containers, cannot forward connections if not running and using the agent.
if !d.IsRunning() {
return nil, errors.New("Instance is not running")
}
// Connect to the agent.
client, err := d.getAgentClient()
if err != nil {
return nil, err
}
// Get the HTTP transport.
httpTransport, ok := client.Transport.(*http.Transport)
if !ok {
return nil, errors.New("Agent client transport is not an *http.Transport")
}
// Send the upgrade request.
u, err := url.Parse("https://custom.socket/1.0/port-forward")
if err != nil {
return nil, err
}
body, err := json.Marshal(api.InstancePortForwardPost{Address: address, Port: port})
if err != nil {
return nil, err
}
req := &http.Request{
Method: http.MethodPost,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Host: u.Host,
Body: io.NopCloser(bytes.NewReader(body)),
ContentLength: int64(len(body)),
}
req.Header["Upgrade"] = []string{"tcp"}
req.Header["Connection"] = []string{"Upgrade"}
req.Header.Set("Content-Type", "application/json")
conn, err := httpTransport.DialContext(context.Background(), "tcp", "8443")
if err != nil {
return nil, err
}
tlsConn := tls.Client(conn, httpTransport.TLSClientConfig)
err = tlsConn.Handshake()
if err != nil {
return nil, err
}
err = req.Write(tlsConn)
if err != nil {
return nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(tlsConn), req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusSwitchingProtocols {
// Try to surface the agent error.
apiResp := api.Response{}
err = json.NewDecoder(resp.Body).Decode(&apiResp)
if err == nil && apiResp.Error != "" {
return nil, errors.New(apiResp.Error)
}
return nil, fmt.Errorf("Dialing failed: expected status code 101 got %d", resp.StatusCode)
}
if resp.Header.Get("Upgrade") != "tcp" {
return nil, errors.New("Missing or unexpected Upgrade header in response")
}
return tlsConn, nil
}
// Console gets access to the instance's console.
func (d *qemu) Console(protocol string) (*os.File, chan error, error) {
var path string
switch protocol {
case instance.ConsoleTypeConsole:
path = d.consolePath()
case instance.ConsoleTypeVGA:
info := DriverStatuses()[instancetype.VM].Info
_, spiceSupported := info.Features["spice"]
if !spiceSupported {
return nil, nil, fmt.Errorf("SPICE is not supported by the host")
}
path = d.spicePath()
default:
return nil, nil, fmt.Errorf("Unknown protocol %q", protocol)
}
// When activating the text-based console, swap the backend to be a socket for an interactive connection.
if protocol == instance.ConsoleTypeConsole {
// Look for existing connections and reset.
conn, err := linux.DialUnix(path)
if err == nil {
_ = d.consoleSwapSocketWithRB()
_ = conn.Close()
// Allow for cleanup to complete on the existing connection.
time.Sleep(time.Second)
}
err = d.consoleSwapRBWithSocket()
if err != nil {
_ = d.consoleSwapSocketWithRB()
return nil, nil, fmt.Errorf("Failed to swap console ring buffer with socket: %w", err)
}
}
// Disconnection notification.
chDisconnect := make(chan error, 1)
// Open the console socket.
conn, err := linux.DialUnix(path)
if err != nil {
if protocol == instance.ConsoleTypeConsole {
_ = d.consoleSwapSocketWithRB()
}
return nil, nil, fmt.Errorf("Connect to console socket %q: %w", path, err)
}
file, err := conn.File()
if err != nil {
if protocol == instance.ConsoleTypeConsole {
_ = d.consoleSwapSocketWithRB()
}
return nil, nil, fmt.Errorf("Get socket file: %w", err)
}
_ = conn.Close()
// Handle disconnections.
go func() {
<-chDisconnect
_ = d.consoleSwapSocketWithRB()
}()
// Only emit a lifecycle event for the text console here. SPICE clients open one socket per channel
// (display, cursor, inputs, ...) and would otherwise produce a flurry of instance-console events
// for a single user session; the VGA emit is handled once per session by the console request handler.
if protocol == instance.ConsoleTypeConsole {
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceConsole.Event(d, logger.Ctx{"type": protocol}))
}
return file, chDisconnect, nil
}
// Exec a command inside the instance.
func (d *qemu) Exec(req api.InstanceExecPost, stdin *os.File, stdout *os.File, stderr *os.File) (instance.Cmd, error) {
reverter := revert.New()
defer reverter.Fail()
client, err := d.getAgentClient()
if err != nil {
return nil, err
}
agent, err := incus.ConnectIncusHTTP(nil, client)
if err != nil {
d.logger.Error("Failed to connect to the agent", logger.Ctx{"err": err})
return nil, errors.New("Failed to connect to the agent")
}
reverter.Add(agent.Disconnect)
dataDone := make(chan bool)
controlSendCh := make(chan api.InstanceExecControl)
controlResCh := make(chan error)
// This is the signal control handler, it receives signals from lxc CLI and forwards them to the VM agent.
controlHandler := func(control *websocket.Conn) {
closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
defer logger.WarnOnError(func() error { return control.WriteMessage(websocket.CloseMessage, closeMsg) }, "Failed to write close message")
for {
select {
case cmd := <-controlSendCh:
controlResCh <- control.WriteJSON(cmd)
case <-dataDone:
return
}
}
}
args := incus.InstanceExecArgs{
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
DataDone: dataDone,
Control: controlHandler,
}
// Always needed for VM exec, as even for non-websocket requests from the client we need to connect the
// websockets for control and for capturing output to a file on the server.
req.WaitForWS = true
// Similarly, output recording is performed on the host rather than in the guest, so clear that bit from the request.
req.RecordOutput = false
op, err := agent.ExecInstance("", req, &args)
if err != nil {
return nil, err
}
instCmd := &qemuCmd{
cmd: op,
attachedChildPid: 0, // Process is not running on the host.
dataDone: args.DataDone,
cleanupFunc: reverter.Clone().Fail, // Pass revert function clone as clean up function.
controlSendCh: controlSendCh,
controlResCh: controlResCh,
}
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceExec.Event(d, logger.Ctx{"command": req.Command}))
reverter.Success()
return instCmd, nil
}
// RenderWithUsage renders the API response including disk usage.
func (d *qemu) RenderWithUsage() (any, any, error) {
resp, etag, err := d.Render()
if err != nil {
return nil, nil, err
}
// Currently only snapshot data needs usage added.
snapResp, ok := resp.(*api.InstanceSnapshot)
if !ok {
return resp, etag, nil
}
pool, err := d.getStoragePool()
if err != nil {
return nil, nil, err
}
// It is important that the snapshot not be mounted here as mounting a snapshot can trigger a very
// expensive filesystem UUID regeneration, so we rely on the driver implementation to get the info
// we are requesting as cheaply as possible.
volumeState, err := pool.GetInstanceUsage(d)
if err != nil {
return resp, etag, nil
}
// A negative usage means the driver couldn't determine it, so leave the size unset.
if volumeState.Used >= 0 {
snapResp.Size = volumeState.Used
}
return snapResp, etag, nil
}
// Render returns info about the instance.
func (d *qemu) Render() (any, any, error) {
profileNames := make([]string, 0, len(d.profiles))
for _, profile := range d.profiles {
profileNames = append(profileNames, profile.Name)
}
if d.IsSnapshot() {
// Prepare the response.
snapState := api.InstanceSnapshot{
CreatedAt: d.creationDate,
Description: d.description,
ExpandedConfig: d.expandedConfig,
ExpandedDevices: d.expandedDevices.CloneNative(),
LastUsedAt: d.lastUsedDate,
Name: strings.SplitN(d.name, "/", 2)[1],
Stateful: d.stateful,
Size: -1, // Default to uninitialized/error state (0 means no CoW usage).
}
snapState.Architecture = d.architectureName
snapState.Config = d.localConfig
snapState.Devices = d.localDevices.CloneNative()
snapState.Ephemeral = d.ephemeral
snapState.Profiles = profileNames
snapState.ExpiresAt = d.expiryDate
return &snapState, d.ETag(), nil
}
// Prepare the response.
statusCode := d.statusCode()
instState := api.Instance{
ExpandedConfig: d.expandedConfig,
ExpandedDevices: d.expandedDevices.CloneNative(),
Name: d.name,
Status: statusCode.String(),
StatusCode: statusCode,
Location: d.node,
Type: d.Type().String(),
}
instState.Description = d.description
instState.Architecture = d.architectureName
instState.Config = d.localConfig
instState.CreatedAt = d.creationDate
instState.Devices = d.localDevices.CloneNative()
instState.Ephemeral = d.ephemeral
instState.LastUsedAt = d.lastUsedDate
instState.Profiles = profileNames
instState.Stateful = d.stateful
instState.Project = d.project.Name
return &instState, d.ETag(), nil
}
// RenderFull returns all info about the instance.
func (d *qemu) RenderFull(hostInterfaces []net.Interface) (*api.InstanceFull, any, error) {
if d.IsSnapshot() {
return nil, nil, errors.New("RenderFull doesn't work with snapshots")
}
// Get the Instance struct.
base, etag, err := d.Render()
if err != nil {
return nil, nil, err
}
// Convert to InstanceFull.
vmState := api.InstanceFull{Instance: *base.(*api.Instance)}
// Add the InstanceState.
vmState.State, err = d.renderState(vmState.StatusCode)
if err != nil {
return nil, nil, err
}
// Add the InstanceSnapshots.
snaps, err := d.Snapshots()
if err != nil {
return nil, nil, err
}
for _, snap := range snaps {
render, _, err := snap.Render()
if err != nil {
return nil, nil, err
}
if vmState.Snapshots == nil {
vmState.Snapshots = []api.InstanceSnapshot{}
}
vmState.Snapshots = append(vmState.Snapshots, *render.(*api.InstanceSnapshot))
}
// Add the InstanceBackups.
backups, err := d.Backups()
if err != nil {
return nil, nil, err
}
for _, backup := range backups {
render := backup.Render()
if vmState.Backups == nil {
vmState.Backups = []api.InstanceBackup{}
}
vmState.Backups = append(vmState.Backups, *render)
}
return &vmState, etag, nil
}
// renderState returns just state info about the instance.
func (d *qemu) renderState(statusCode api.StatusCode) (*api.InstanceState, error) {
// Initialize the return struct.
status := &api.InstanceState{
Processes: -1,
Status: statusCode.String(),
StatusCode: statusCode,
}
// If VM is stopped or errored, we're done here.
if d.isErrorStatusCode(statusCode) || !d.isRunningStatusCode(statusCode) {
diskState, err := d.diskState()
if err != nil && !errors.Is(err, storageDrivers.ErrNotSupported) {
d.logger.Warn("Error getting disk usage", logger.Ctx{"err": err})
}
status.Disk = diskState
return status, nil
}
// If possible, get the metrics from the agent.
if d.agentMetricsEnabled() {
agentStatus, err := d.agentGetState()
if err != nil {
if !errors.Is(err, errQemuAgentOffline) {
d.logger.Warn("Could not get VM state from agent", logger.Ctx{"err": err})
} else {
monitor, err := d.qmpConnect()
if err == nil && monitor.AgenStarted() {
agentStatus = monitor.GetInstanceState()
}
}
}
if agentStatus != nil {
status = agentStatus
}
}
// Override VM state back to QEMU state.
status.Status = statusCode.String()
status.StatusCode = statusCode
// Add the network details if missing.
if len(status.Network) == 0 {
networkState, err := d.getNetworkState()
if err != nil {
return nil, err
}
status.Network = networkState
}
// Add the memory details if missing.
if status.Memory.Usage <= 0 {
monitor, err := d.qmpConnect()
if err != nil {
d.logger.Warn("Error getting QEMU monitor", logger.Ctx{"err": err})
}
memoryMetrics, err := d.getQemuMemoryMetrics(monitor)
if err != nil {
d.logger.Warn("Error getting memory metrics", logger.Ctx{"err": err})
}
status.Memory.Total = int64(memoryMetrics.MemTotalBytes)
status.Memory.Usage = int64(memoryMetrics.MemTotalBytes - memoryMetrics.MemAvailableBytes)
}
// Populate the disk information.
diskState, err := d.diskState()
if err != nil && !errors.Is(err, storageDrivers.ErrNotSupported) {
d.logger.Warn("Error getting disk usage", logger.Ctx{"err": err})
}
status.Disk = diskState
// Populate the CPU time allocation.
limitsCPU, ok := d.expandedConfig["limits.cpu"]
if ok {
cpuCount, err := strconv.ParseInt(limitsCPU, 10, 64)
if err != nil {
status.CPU.AllocatedTime = cpuCount * 1_000_000_000
}
} else {
status.CPU.AllocatedTime = qemudefault.CPUCores * 1_000_000_000
}
// Populate host_name for network devices.
for k, m := range d.ExpandedDevices() {
// We only care about nics.
if m["type"] != "nic" {
continue
}
// Get hwaddr from static or volatile config.
hwaddr := m["hwaddr"]
if hwaddr == "" {
hwaddr = d.localConfig[fmt.Sprintf("volatile.%s.hwaddr", k)]
}
// We have to match on hwaddr as device name can be different from the configured device
// name when reported from the agent inside the VM (due to the guest OS choosing name).
for netName, netStatus := range status.Network {
if netStatus.Hwaddr == hwaddr {
if netStatus.HostName == "" {
netStatus.HostName = d.localConfig[fmt.Sprintf("volatile.%s.host_name", k)]
status.Network[netName] = netStatus
}
}
}
}
// Populate the process information.
pid, _ := d.pid()
status.Pid = int64(pid)
status.StartedAt, err = d.processStartedAt(d.InitPID())
if err != nil {
return status, err
}
return status, nil
}
// RenderState returns just state info about the instance.
func (d *qemu) RenderState(hostInterfaces []net.Interface) (*api.InstanceState, error) {
return d.renderState(d.statusCode())
}
// diskState gets disk usage info.
func (d *qemu) diskState() (map[string]api.InstanceStateDisk, error) {
pool, err := d.getStoragePool()
if err != nil {
return nil, err
}
// Get the root disk device config.
rootDiskName, _, err := d.getRootDiskDevice()
if err != nil {
return nil, err
}
usage, err := pool.GetInstanceUsage(d)
if err != nil {
return nil, err
}
disk := map[string]api.InstanceStateDisk{}
disk[rootDiskName] = api.InstanceStateDisk{
Usage: usage.Used,
Total: usage.Total,
}
return disk, nil
}
// agentGetState connects to the agent inside of the VM and does
// an API call to get the current state.
func (d *qemu) agentGetState() (*api.InstanceState, error) {
client, err := d.getAgentClient()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
agent, err := incus.ConnectIncusHTTPWithContext(ctx, nil, client)
if err != nil {
return nil, fmt.Errorf("Failed connecting to agent: %w", err)
}
defer agent.Disconnect()
status, _, err := agent.GetInstanceState("")
if err != nil {
return nil, err
}
return status, nil
}
// IsRunning returns whether or not the instance is running.
func (d *qemu) IsRunning() bool {
return d.isRunningStatusCode(d.statusCode())
}
// IsFrozen returns whether the instance frozen or not.
func (d *qemu) IsFrozen() bool {
return d.statusCode() == api.Frozen
}
// CanMigrate returns whether the instance can be migrated.
func (d *qemu) CanMigrate() string {
return d.canMigrate(d)
}
// LockExclusive attempts to get exclusive access to the instance's root volume.
func (d *qemu) LockExclusive() (*operationlock.InstanceOperation, error) {
if d.IsRunning() {
return nil, errors.New("Instance is running")
}
// Prevent concurrent operations the instance.
op, err := operationlock.Create(d.Project().Name, d.Name(), d.op, operationlock.ActionCreate, false, false)
if err != nil {
return nil, err
}
return op, err
}
// DeviceEventHandler handles events occurring on the instance's devices.
func (d *qemu) DeviceEventHandler(runConf *deviceConfig.RunConfig) error {
if !d.IsRunning() || runConf == nil {
return nil
}
// Handle uevents.
for _, uevent := range runConf.Uevents {
for _, event := range uevent {
fields := strings.SplitN(event, "=", 2)
if fields[0] != "ACTION" {
continue
}
switch fields[1] {
case "add":
for _, usbDev := range runConf.USBDevice {
// This ensures that the device is actually removed from QEMU before adding it again.
// In most cases the device will already be removed, but it is possible that the
// device still exists in QEMU before trying to add it again.
// If a USB device is physically detached from a running VM while the server
// itself is stopped, QEMU in theory will not delete the device.
err := d.deviceDetachUSB(usbDev)
if err != nil {
return err
}
err = d.deviceAttachUSB(usbDev)
if err != nil {
return err
}
}
case "remove":
for _, usbDev := range runConf.USBDevice {
err := d.deviceDetachUSB(usbDev)
if err != nil {
return err
}
}
}
}
}
// Handle disk reconfiguration.
for _, mount := range runConf.Mounts {
if mount.Limits == nil && mount.Size == 0 {
continue
}
// Get the QMP monitor.
m, err := d.qmpConnect()
if err != nil {
return err
}
// Figure out the QEMU device ID.
devID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, linux.PathNameEncode(mount.DevName))
if mount.Limits != nil {
// Apply the limits.
err = m.SetBlockThrottle(devID, int(mount.Limits.ReadBytes), int(mount.Limits.WriteBytes), int(mount.Limits.ReadIOps), int(mount.Limits.WriteIOps))
if err != nil {
return fmt.Errorf("Failed applying limits for disk device %q: %w", mount.DevName, err)
}
}
if mount.Size > 0 {
// Update the size.
err = m.UpdateBlockSize(strings.SplitN(devID, "-", 2)[1], mount.Size)
if err != nil {
return fmt.Errorf("Failed updating disk size %q: %w", mount.DevName, err)
}
}
}
// Handle NIC reconfiguration.
var devName string
var connected bool
for _, dev := range runConf.NetworkInterface {
switch dev.Key {
case "devName":
devName = dev.Value
case "connected":
connected = util.IsTrueOrEmpty(dev.Value)
}
}
if devName != "" {
// Get the QMP monitor.
m, err := d.qmpConnect()
if err != nil {
return err
}
// Figure out the QEMU device ID.
devID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, linux.PathNameEncode(devName))
err = m.SetNICLink(devID, connected)
if err != nil {
return fmt.Errorf("Failed setting NIC device link status: %w", err)
}
}
return nil
}
// reservedVsockID returns true if the given vsockID equals 0, 1 or 2.
// Those are reserved and we cannot use them.
func (d *qemu) reservedVsockID(vsockID uint32) bool {
return vsockID <= 2
}
// getVsockID returns the vsock Context ID for the VM.
func (d *qemu) getVsockID() (uint32, error) {
existingVsockID, ok := d.localConfig["volatile.vsock_id"]
if !ok {
return 0, errors.New("Context ID not set in volatile.vsock_id")
}
vsockID, err := strconv.ParseUint(existingVsockID, 10, 32)
if err != nil {
return 0, fmt.Errorf("Failed to parse volatile.vsock_id: %q: %w", existingVsockID, err)
}
if d.reservedVsockID(uint32(vsockID)) {
return 0, fmt.Errorf("Failed to use reserved vsock Context ID: %d", vsockID)
}
return uint32(vsockID), nil
}
// acquireVsockID tries to occupy the given vsock Context ID.
// If the ID is free it returns the corresponding file handle.
func (d *qemu) acquireVsockID(vsockID uint32) (*os.File, error) {
reverter := revert.New()
defer reverter.Fail()
vsockF, err := os.OpenFile("/dev/vhost-vsock", os.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("Failed to open vhost socket: %w", err)
}
reverter.Add(func() { _ = vsockF.Close() })
// The vsock Context ID cannot be supplied as type uint32.
vsockIDInt := uint64(vsockID)
// Call the ioctl to set the context ID.
_, _, errno := unix.Syscall(unix.SYS_IOCTL, vsockF.Fd(), linux.IoctlVhostVsockSetGuestCid, uintptr(unsafe.Pointer(&vsockIDInt)))
if errno != 0 {
if !errors.Is(errno, unix.EADDRINUSE) {
return nil, fmt.Errorf("Failed ioctl syscall to vhost socket: %q", errno.Error())
}
// vsock Context ID is already in use.
return nil, nil
}
reverter.Success()
return vsockF, nil
}
// acquireExistingVsockID tries to acquire an already existing vsock Context ID from volatile.
// It returns both the acquired ID and opened vsock file handle for QEMU.
func (d *qemu) acquireExistingVsockID() (uint32, *os.File, error) {
vsockID, err := d.getVsockID()
if err != nil {
return 0, nil, err
}
// Check if the vsockID from last VM start is still not acquired in case the VM was stopped.
f, err := d.acquireVsockID(vsockID)
if err != nil {
return 0, nil, err
}
return vsockID, f, nil
}
// nextVsockID tries to acquire the next free vsock Context ID for the VM.
// It returns both the acquired ID and opened vsock file handle for QEMU.
func (d *qemu) nextVsockID() (uint32, *os.File, error) {
// Check if vsock ID from last VM start is present in volatile, then use that.
// This allows a running VM to be recovered after DB record deletion and that an agent connection still works
// after the VM's instance ID has changed.
// Continue in case of error since the caller requires a valid vsockID in any case.
vsockID, vsockF, _ := d.acquireExistingVsockID()
if vsockID != 0 && vsockF != nil {
return vsockID, vsockF, nil
}
// Ignore the error from before and start to acquire a new Context ID.
instanceUUID, err := uuid.Parse(d.localConfig["volatile.uuid"])
if err != nil {
return 0, nil, fmt.Errorf("Failed to parse instance UUID from volatile.uuid: %w", err)
}
r, err := localUtil.GetStableRandomGenerator(instanceUUID.String())
if err != nil {
return 0, nil, fmt.Errorf("Failed generating stable random seed from instance UUID %q: %w", instanceUUID, err)
}
timeout := time.Now().Add(5 * time.Second)
// Try to find a new Context ID.
for {
if time.Now().After(timeout) {
return 0, nil, errors.New("Timeout exceeded whilst trying to acquire the next vsock Context ID")
}
candidateVsockID := r.Uint32()
if d.reservedVsockID(candidateVsockID) {
continue
}
vsockF, err := d.acquireVsockID(candidateVsockID)
if err != nil {
return 0, nil, err
}
if vsockF != nil {
return candidateVsockID, vsockF, nil
}
}
}
// InitPID returns the instance's current process ID.
func (d *qemu) InitPID() int {
pid, _ := d.pid()
return pid
}
func (d *qemu) statusCode() api.StatusCode {
// Shortcut to avoid spamming QMP during ongoing operations.
op := operationlock.Get(d.Project().Name, d.Name())
if op != nil {
if op.Action() == operationlock.ActionStart {
return api.Stopped
}
if op.Action() == operationlock.ActionStop {
if util.IsTrue(d.LocalConfig()["volatile.last_state.ready"]) {
return api.Ready
}
return api.Running
}
}
// Connect to the monitor.
monitor, err := d.qmpConnect()
if err != nil {
// If cannot connect to monitor, but qemu process in pid file still exists, then likely qemu
// is unresponsive and this instance is in an error state.
pid, _ := d.pid()
if pid > 0 {
return api.Error
}
// If we fail to connect, chances are the VM isn't running.
return api.Stopped
}
status, err := monitor.Status()
if err != nil {
if errors.Is(err, qmp.ErrMonitorDisconnect) {
// If cannot connect to monitor, but qemu process in pid file still exists, then likely
// qemu is unresponsive and this instance is in an error state.
pid, _ := d.pid()
if pid > 0 {
return api.Error
}
return api.Stopped
}
return api.Error
}
switch status {
case "prelaunch", "running":
if status == "running" && util.IsTrue(d.LocalConfig()["volatile.last_state.ready"]) {
return api.Ready
}
return api.Running
case "inmigrate", "postmigrate", "finish-migrate", "save-vm", "suspended", "paused":
return api.Frozen
default:
return api.Error
}
}
// State returns the instance's state code.
func (d *qemu) State() string {
return strings.ToUpper(d.statusCode().String())
}
// EarlyLogFilePath returns the instance's early log path.
func (d *qemu) EarlyLogFilePath() string {
return filepath.Join(d.LogPath(), "qemu.early.log")
}
// LogFilePath returns the instance's log path.
func (d *qemu) LogFilePath() string {
return filepath.Join(d.LogPath(), "qemu.log")
}
// QMPLogFilePath returns the instance's QMP log path.
func (d *qemu) QMPLogFilePath() string {
return filepath.Join(d.LogPath(), "qemu.qmp.log")
}
// FillNetworkDevice takes a nic or infiniband device type and enriches it with automatically
// generated name and hwaddr properties if these are missing from the device.
func (d *qemu) FillNetworkDevice(name string, m deviceConfig.Device) (deviceConfig.Device, error) {
var err error
newDevice := m.Clone()
nicType, err := nictype.NICType(d.state, d.Project().Name, m)
if err != nil {
return nil, err
}
isPhysicalWithBridge := device.IsPhysicalNICWithBridge(d.state, d.Project().Name, m)
// Fill in the MAC address.
if (!slices.Contains([]string{"physical", "ipvlan"}, nicType) || isPhysicalWithBridge) && m["hwaddr"] == "" {
configKey := fmt.Sprintf("volatile.%s.hwaddr", name)
volatileHwaddr := d.localConfig[configKey]
if volatileHwaddr == "" {
// Generate a new MAC address.
volatileHwaddr, err = instance.DeviceNextInterfaceHWAddr(d.MACPattern())
if err != nil || volatileHwaddr == "" {
return nil, fmt.Errorf("Failed generating %q: %w", configKey, err)
}
// Update the database and update volatileHwaddr with stored value.
volatileHwaddr, err = d.insertConfigkey(configKey, volatileHwaddr)
if err != nil {
return nil, fmt.Errorf("Failed storing generated config key %q: %w", configKey, err)
}
// Set stored value into current instance config.
d.localConfig[configKey] = volatileHwaddr
d.expandedConfig[configKey] = volatileHwaddr
}
if volatileHwaddr == "" {
return nil, fmt.Errorf("Failed getting %q", configKey)
}
newDevice["hwaddr"] = volatileHwaddr
}
return newDevice, nil
}
// UpdateBackupFile writes the instance's backup.yaml file to storage.
func (d *qemu) UpdateBackupFile() error {
// Prevent concurrent updates to the backup file.
unlock, err := d.updateBackupFileLock(context.Background())
if err != nil {
return err
}
defer unlock()
// Write the current instance state to backup file.
pool, err := d.getStoragePool()
if err != nil {
return err
}
return pool.UpdateInstanceBackupFile(d, true, nil)
}
func (d *qemu) devIncusEventSend(eventType string, eventMessage map[string]any) error {
event := jmap.Map{}
event["type"] = eventType
event["timestamp"] = time.Now()
event["metadata"] = eventMessage
client, err := d.getAgentClient()
if err != nil {
// Don't fail if the VM simply doesn't have an agent.
if errors.Is(err, errQemuAgentOffline) {
return nil
}
return err
}
agentArgs := &incus.ConnectionArgs{
SkipGetEvents: true,
SkipGetServer: true,
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
agent, err := incus.ConnectIncusHTTPWithContext(ctx, agentArgs, client)
if err != nil {
d.logger.Error("Failed to connect to the agent", logger.Ctx{"err": err})
return errors.New("Failed to connect to the agent")
}
defer agent.Disconnect()
_, _, err = agent.RawQuery("POST", "/1.0/events", &event, "")
if err != nil {
return err
}
return nil
}
// Info returns "qemu" and the currently loaded qemu version.
func (d *qemu) Info() instance.Info {
data := instance.Info{
Name: "qemu",
Features: make(map[string]any),
Type: instancetype.VM,
Error: errors.New("Unknown error"),
}
if !util.PathExists("/dev/kvm") {
data.Error = errors.New("KVM support is missing (no /dev/kvm)")
return data
}
err := linux.LoadModule("vhost_vsock")
if err != nil {
data.Error = errors.New("vhost_vsock kernel module not loaded")
return data
}
if !util.PathExists("/dev/vsock") {
data.Error = errors.New("Vsock support is missing (no /dev/vsock)")
return data
}
hostArch, err := osarch.ArchitectureGetLocalID()
if err != nil {
logger.Errorf("Failed getting CPU architecture during QEMU initialization: %v", err)
data.Error = errors.New("Failed getting CPU architecture")
return data
}
qemuPath, _, err := d.qemuArchConfig(hostArch)
if err != nil {
data.Error = fmt.Errorf("QEMU command not available: %v", err)
return data
}
out, err := exec.Command(qemuPath, "--version").Output()
if err != nil {
logger.Errorf("Failed getting version during QEMU initialization: %v", err)
data.Error = errors.New("Failed getting QEMU version")
return data
}
qemuOutput := strings.Fields(string(out))
if len(qemuOutput) >= 4 {
qemuVersion := strings.Fields(string(out))[3]
data.Version = qemuVersion
} else {
data.Version = "unknown" // Not necessarily an error that should prevent us using driver.
}
data.Features, err = d.checkFeatures(hostArch, qemuPath)
if err != nil {
logger.Errorf("Unable to run feature checks during QEMU initialization: %v", err)
data.Error = errors.New("QEMU failed to run feature checks")
return data
}
data.Error = nil
return data
}
func (d *qemu) checkFeatures(hostArch int, qemuPath string) (map[string]any, error) {
monitorPath, err := os.CreateTemp("", "")
if err != nil {
return nil, err
}
defer logger.WarnOnError(func() error { return os.Remove(monitorPath.Name()) }, "Failed to remove monitor file")
qemuArgs := []string{
qemuPath,
"-S", // Do not start virtualisation.
"-nographic",
"-nodefaults",
"-no-user-config",
"-chardev", fmt.Sprintf("socket,id=monitor,path=%s,server=on,wait=off", qemuEscapeCmdline(monitorPath.Name())),
"-mon", "chardev=monitor,mode=control",
"-machine", qemuMachineType(hostArch),
}
if hostArch == osarch.ARCH_64BIT_INTEL_X86 {
// On Intel, use KVM acceleration as it's needed for SEV detection.
// This also happens to be less resource intensive but can't
// trivially be performed on all architectures without extra care about the
// machine type.
qemuArgs = append(qemuArgs, "-accel", "kvm")
}
if d.architectureSupportsUEFI(hostArch) {
// Try to locate a UEFI firmware.
var efiPath string
firmwares, err := edk2.GetArchitectureFirmwarePairsForUsage(hostArch, edk2.GENERIC)
if err != nil {
return nil, err
}
for _, firmwarePair := range firmwares {
if util.PathExists(firmwarePair.Code) {
efiPath = firmwarePair.Code
break
}
}
if efiPath == "" {
return nil, errors.New("Unable to locate a UEFI firmware")
}
qemuArgs = append(qemuArgs, "-drive", fmt.Sprintf("if=pflash,format=raw,readonly=on,file=%s", qemuEscapeCmdline(efiPath)))
}
var stderr bytes.Buffer
checkFeature := exec.Cmd{
Path: qemuPath,
Args: qemuArgs,
Stderr: &stderr,
}
err = checkFeature.Start()
if err != nil {
// QEMU not operational. VM support missing.
return nil, fmt.Errorf("Failed starting QEMU: %w", err)
}
defer logger.WarnOnError(checkFeature.Process.Kill, "Failed to kill QEMU process")
// Start go routine that waits for QEMU to exit and captures the exit error (if any).
errWaitCh := make(chan error, 1)
go func() {
errWaitCh <- checkFeature.Wait()
}()
// Start go routine that tries to connect to QEMU's QMP socket in a loop (giving QEMU a chance to open it).
ctx, cancelMonitorConnect := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelMonitorConnect()
errMonitorCh := make(chan error, 1)
var monitor *qmp.Monitor
go func() {
var err error
// Try and connect to QMP socket until cancelled.
for {
monitor, err = qmp.Connect(monitorPath.Name(), qemuSerialChardevName, nil, "", d.detachDisk)
// QMP successfully connected or we have been cancelled.
if err == nil || ctx.Err() != nil {
break
}
time.Sleep(50 * time.Millisecond)
}
// Return last QMP connection error.
errMonitorCh <- err
}()
// Wait for premature QEMU exit or QMP to connect or timeout.
select {
case errMonitor := <-errMonitorCh:
// A non-nil error here means that QMP failed to connect before timing out.
// The last connection error is returned.
// A nil error means QMP successfully connected and we can continue.
if errMonitor != nil {
return nil, fmt.Errorf("QEMU monitor connect error: %w", errMonitor)
}
case errWait := <-errWaitCh:
// Any sort of premature exit, even a non-error one is problematic here, and should not occur.
return nil, fmt.Errorf("QEMU premature exit: %w (%v)", errWait, strings.TrimSpace(stderr.String()))
}
defer monitor.Disconnect()
features := make(map[string]any)
blockDevPath, err := os.CreateTemp("", "")
if err != nil {
return nil, err
}
defer logger.WarnOnError(func() error { return os.Remove(blockDevPath.Name()) }, "Failed to remove block device file")
// Check io_uring feature.
blockDev := map[string]any{
"node-name": d.blockNodeName("feature-check"),
"driver": "file",
"filename": blockDevPath.Name(),
"aio": "io_uring",
}
err = monitor.AddBlockDevice(blockDev, nil, false)
if err != nil {
logger.Debug("Failed adding block device during VM feature check", logger.Ctx{"err": err})
} else {
features["io_uring"] = struct{}{}
}
// Check CPU hotplug feature.
_, err = monitor.QueryHotpluggableCPUs()
if err != nil {
logger.Debug("Failed querying hotpluggable CPUs during VM feature check", logger.Ctx{"err": err})
} else {
features["cpu_hotplug"] = struct{}{}
}
// Check AMD SEV features (only for x86 architecture)
if hostArch == osarch.ARCH_64BIT_INTEL_X86 {
cmdline, err := os.ReadFile("/proc/cmdline")
if err != nil {
return nil, err
}
parts := strings.Split(string(cmdline), " ")
// Check if SME is enabled in the kernel command line. // codespell:ignore sme
if slices.Contains(parts, "mem_encrypt=on") || util.PathExists("/dev/sev") {
features["sme"] = struct{}{} // codespell:ignore sme
}
// Check if SEV/SEV-ES are enabled
sev, err := os.ReadFile("/sys/module/kvm_amd/parameters/sev")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
} else if strings.TrimSpace(string(sev)) == "Y" {
// Host supports SEV, check if QEMU supports it as well.
capabilities, err := monitor.SEVCapabilities()
if err != nil {
logger.Debug("Failed querying SEV capability during VM feature check", logger.Ctx{"err": err})
} else {
features["sev"] = capabilities
// If SEV is enabled on host and supported by QEMU,
// check if the SEV-ES extension is enabled.
sevES, err := os.ReadFile("/sys/module/kvm_amd/parameters/sev_es")
if err != nil {
logger.Debug("Failed querying SEV-ES capability during VM feature check", logger.Ctx{"err": err})
} else if strings.TrimSpace(string(sevES)) == "Y" {
features["sev-es"] = struct{}{}
}
}
}
}
// Check if vhost-net accelerator (for NIC CPU offloading) is available.
if util.PathExists("/dev/vhost-net") {
features["vhost_net"] = struct{}{}
}
// Check if SPICE is compiled into QEMU.
err = monitor.QuerySpice()
if err != nil {
logger.Debug("Failed querying SPICE during VM feature check", logger.Ctx{"err": err})
} else {
features["spice"] = struct{}{}
}
// Check if virtio-9p-pci is compiled into QEMU.
err = monitor.Query9pDevice()
if err != nil {
logger.Debug("Failed querying virtio-9p-pci during VM feature check", logger.Ctx{"err": err})
} else {
features["plan9"] = struct{}{}
}
// Check if virtio-sound-pci is compiled into QEMU.
err = monitor.QueryVirtioSoundDevice()
if err != nil {
logger.Debug("Failed querying virtio-sound-pci during VM feature check", logger.Ctx{"err": err})
} else {
features["virtio-sound"] = struct{}{}
}
// Check if virtio-vga is compiled into QEMU.
err = monitor.QueryVirtioVGADevice()
if err != nil {
logger.Debug("Failed querying virtio-vga during VM feature check", logger.Ctx{"err": err})
} else {
features["virtio-vga"] = struct{}{}
}
// Check if running nested.
cpus, err := resources.GetCPU()
if err != nil {
return nil, err
}
nested := false
for _, socket := range cpus.Sockets {
for _, core := range socket.Cores {
if slices.Contains(core.Flags, "hypervisor") {
nested = true
}
}
}
if nested {
features["nested"] = struct{}{}
}
// Get the host CPU model (x86_64 only for now).
if hostArch == osarch.ARCH_64BIT_INTEL_X86 {
model, err := monitor.QueryCPUModel("kvm64")
if err != nil {
// Fallback to qemu64 if kvm64 is missing (RHEL).
model, err = monitor.QueryCPUModel("qemu64")
if err != nil {
return nil, err
}
}
cpuFlags := map[string]bool{}
for k, v := range model.Flags {
value, ok := v.(bool)
if !ok {
continue
}
cpuFlags[k] = value
}
features["flags"] = cpuFlags
}
return features, nil
}
// version returns the QEMU version.
func (d *qemu) version() (*version.DottedVersion, error) {
info := DriverStatuses()[instancetype.VM].Info
qemuVer, err := version.NewDottedVersion(info.Version)
if err != nil {
return nil, fmt.Errorf("Failed parsing QEMU version: %w", err)
}
return qemuVer, nil
}
// Metrics returns the metrics set for the instance.
func (d *qemu) Metrics(hostInterfaces []net.Interface) (*metrics.MetricSet, error) {
if !d.IsRunning() {
return nil, ErrInstanceIsStopped
}
if d.agentMetricsEnabled() {
agentMetrics, err := d.getAgentMetrics()
if err != nil {
if !errors.Is(err, errQemuAgentOffline) {
d.logger.Warn("Could not get VM metrics from agent", logger.Ctx{"err": err})
}
// Fallback data if agent is not reachable.
return d.getQemuMetrics()
}
return agentMetrics, nil
}
return d.getQemuMetrics()
}
func (d *qemu) getAgentMetrics() (*metrics.MetricSet, error) {
client, err := d.getAgentClient()
if err != nil {
return nil, err
}
agentArgs := &incus.ConnectionArgs{
SkipGetEvents: true,
SkipGetServer: true,
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
agent, err := incus.ConnectIncusHTTPWithContext(ctx, agentArgs, client)
if err != nil {
d.logger.Error("Failed to connect to the agent", logger.Ctx{"project": d.Project().Name, "instance": d.Name(), "err": err})
return nil, errors.New("Failed to connect to the agent")
}
defer agent.Disconnect()
resp, _, err := agent.RawQuery("GET", "/1.0/metrics", nil, "")
if err != nil {
return nil, err
}
var m metrics.Metrics
err = json.Unmarshal(resp.Metadata, &m)
if err != nil {
return nil, err
}
metricSet, err := metrics.MetricSetFromAPI(&m, map[string]string{"project": d.project.Name, "name": d.name, "type": instancetype.VM.String()})
if err != nil {
return nil, err
}
return metricSet, nil
}
func (d *qemu) getNetworkState() (map[string]api.InstanceStateNetwork, error) {
networks := map[string]api.InstanceStateNetwork{}
for k, m := range d.ExpandedDevices() {
if m["type"] != "nic" {
continue
}
dev, err := d.deviceLoad(d, k, m, false)
if err != nil {
if errors.Is(err, device.ErrUnsupportedDevType) {
continue // Skip unsupported device (allows for mixed instance type profiles).
}
d.logger.Warn("Failed state validation for device", logger.Ctx{"device": k, "err": err})
continue
}
// Only some NIC types support fallback state mechanisms when there is no agent.
nic, ok := dev.(device.NICState)
if !ok {
continue
}
nicState, err := nic.State()
if err != nil {
return nil, fmt.Errorf("Failed getting NIC state for %q: %w", k, err)
}
if nicState != nil {
networks[k] = *nicState
}
}
return networks, nil
}
func (d *qemu) agentMetricsEnabled() bool {
return util.IsTrueOrEmpty(d.expandedConfig["security.agent.metrics"])
}
func (d *qemu) deviceAttachUSB(usbConf deviceConfig.USBDeviceItem) error {
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
monHook, err := d.addUSBDeviceConfig(usbConf)
if err != nil {
return err
}
err = monHook(monitor)
if err != nil {
return err
}
return nil
}
func (d *qemu) deviceDetachUSB(usbDev deviceConfig.USBDeviceItem) error {
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
deviceID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, usbDev.DeviceName)
err = monitor.RemoveDevice(deviceID)
if err != nil && !api.StatusErrorCheck(err, http.StatusNotFound) {
return fmt.Errorf("Failed removing device: %w", err)
}
err = monitor.RemoveFDFromFDSet(deviceID)
if err != nil {
return fmt.Errorf("Failed removing FD set: %w", err)
}
return nil
}
// Block node names may only be up to 31 characters long, so use a hash if longer.
func (d *qemu) blockNodeName(name string) string {
// Apply the prefix.
return fmt.Sprintf("%s%s", qemuBlockDevIDPrefix, hashValue(name, 25))
}
// Mount tag names may only be up to 31 or 36 characters long, so use a hash if longer.
func (d *qemu) mountTagName(name string, maxLength int) string {
// Apply the prefix.
return fmt.Sprintf("%s%s", qemuMountTagPrefix, hashValue(name, maxLength))
}
func (d *qemu) setCPUs(monitor *qmp.Monitor, count int) error {
if count == 0 {
return nil
}
// Check if the agent is running.
if monitor == nil {
var err error
monitor, err = d.qmpConnect()
if err != nil {
return err
}
}
cpus, err := monitor.QueryHotpluggableCPUs()
if err != nil {
return fmt.Errorf("Failed to query hotpluggable CPUs: %w", err)
}
var availableCPUs []qmp.HotpluggableCPU
var hotpluggedCPUs []qmp.HotpluggableCPU
// Count the available and hotplugged CPUs.
for _, cpu := range cpus {
// If qom-path is unset, the CPU is available.
if cpu.QOMPath == "" {
availableCPUs = append(availableCPUs, cpu)
} else if strings.HasPrefix(cpu.QOMPath, "/machine/peripheral") {
hotpluggedCPUs = append(hotpluggedCPUs, cpu)
}
}
// The reserved CPUs includes both the hotplugged CPUs as well as the fixed one.
totalReservedCPUs := len(hotpluggedCPUs) + 1
// Nothing to do as the count matches the already reserved CPUs.
if count == totalReservedCPUs {
return nil
}
reverter := revert.New()
defer reverter.Fail()
// More CPUs requested.
if count > totalReservedCPUs {
// Cannot allocate more CPUs than the system provides.
if count > len(cpus) {
return fmt.Errorf("Requested CPU count of %d exceeds instance current maximum of %d, restart required", count, len(cpus))
}
// This shouldn't trigger, but if it does, don't panic.
if count-totalReservedCPUs > len(availableCPUs) {
return errors.New("Unable to allocate more CPUs, not enough hotpluggable CPUs available")
}
// Only allocate the difference in CPUs.
for i := range count - totalReservedCPUs {
cpu := availableCPUs[i]
devID := fmt.Sprintf("cpu%d%d%d", cpu.Props.SocketID, cpu.Props.CoreID, cpu.Props.ThreadID)
qemuDev := map[string]any{
"id": devID,
"driver": cpu.Type,
"core-id": cpu.Props.CoreID,
}
// No such thing as sockets and threads on s390x.
if d.architecture != osarch.ARCH_64BIT_S390_BIG_ENDIAN {
qemuDev["socket-id"] = cpu.Props.SocketID
qemuDev["thread-id"] = cpu.Props.ThreadID
}
err := monitor.AddDevice(qemuDev)
if err != nil {
return fmt.Errorf("Failed to add device: %w", err)
}
reverter.Add(func() {
err := monitor.RemoveDevice(devID)
d.logger.Warn("Failed to remove CPU device", logger.Ctx{"err": err})
})
}
} else {
if totalReservedCPUs-count > len(hotpluggedCPUs) {
// This shouldn't trigger, but if it does, don't panic.
return errors.New("Unable to remove CPUs, not enough hotpluggable CPUs available")
}
// Less CPUs requested.
for i := range totalReservedCPUs - count {
cpu := hotpluggedCPUs[i]
fields := strings.Split(cpu.QOMPath, "/")
devID := fields[len(fields)-1]
err := monitor.RemoveDevice(devID)
if err != nil {
return fmt.Errorf("Failed to remove CPU: %w", err)
}
reverter.Add(func() {
err := monitor.AddDevice(map[string]any{
"id": devID,
"driver": cpu.Type,
"socket-id": cpu.Props.SocketID,
"core-id": cpu.Props.CoreID,
"thread-id": cpu.Props.ThreadID,
})
d.logger.Warn("Failed to add CPU device", logger.Ctx{"err": err})
})
}
// QEMU doesn't immediately remove the thread from the vCPU list.
// Wait a second to allow the thread to fully exit and disappear from the vCPU list.
time.Sleep(time.Second)
}
reverter.Success()
// Run post-hotplug tasks.
err = d.postCPUHotplug(monitor)
if err != nil {
return err
}
return nil
}
// resetSupportThreadsAffinity resets the CPU affinity of all non-vCPU QEMU threads,
// releasing them from the confinement applied during startup.
func (d *qemu) resetSupportThreadsAffinity(monitor *qmp.Monitor, pid int) error {
// Get the vCPU PID list.
vcpuPIDs, err := monitor.GetCPUs()
if err != nil {
return err
}
// Use our own affinity as the reset target.
set := unix.CPUSet{}
err = unix.SchedGetaffinity(0, &set)
if err != nil {
return err
}
// Reset every non-vCPU thread.
entries, err := os.ReadDir(fmt.Sprintf("/proc/%d/task", pid))
if err != nil {
return err
}
for _, entry := range entries {
tid, err := strconv.Atoi(entry.Name())
if err != nil {
continue
}
if slices.Contains(vcpuPIDs, tid) {
continue
}
err = unix.SchedSetaffinity(tid, &set)
if err != nil && !errors.Is(err, unix.ESRCH) {
return err
}
}
return nil
}
func (d *qemu) architectureSupportsCPUHotplug() bool {
// Check supported features.
info := DriverStatuses()[instancetype.VM].Info
_, found := info.Features["cpu_hotplug"]
return found
}
func (d *qemu) postCPUHotplug(monitor *qmp.Monitor) error {
// Get the vCPU PID list.
pids, err := monitor.GetCPUs()
if err != nil {
return err
}
// Handle NUMA node restrictions.
numaNodes := d.expandedConfig["limits.cpu.nodes"]
if numaNodes != "" {
if numaNodes == "balanced" {
numaNodes = d.expandedConfig["volatile.cpu.nodes"]
}
// Parse the NUMA restriction.
numaNodeSet, err := resources.ParseNumaNodeSet(numaNodes)
if err != nil {
return err
}
// Get the CPU topology.
cpusTopology, err := resources.GetCPU()
if err != nil {
return err
}
// Get the isolated CPU ids.
isolatedCpusInt := resources.GetCPUIsolated()
// Build a map of NUMA node to CPU threads.
numaNodeToCPU := make(map[int64][]int64)
for _, cpu := range cpusTopology.Sockets {
for _, core := range cpu.Cores {
for _, thread := range core.Threads {
// Skip any isolated CPU thread.
if slices.Contains(isolatedCpusInt, thread.ID) {
continue
}
numaNodeToCPU[int64(thread.NUMANode)] = append(numaNodeToCPU[int64(thread.NUMANode)], thread.ID)
}
}
}
// Figure out the list of CPU threads for the NUMA node(s).
set := unix.CPUSet{}
for _, numaNode := range numaNodeSet {
for _, id := range numaNodeToCPU[numaNode] {
set.Set(int(id))
}
}
// Apply the restriction.
for _, pid := range pids {
// Apply the pin.
err := unix.SchedSetaffinity(pid, &set)
if err != nil {
return err
}
}
}
// Create a core scheduling group.
err = d.setCoreSched(pids)
if err != nil {
return fmt.Errorf("Failed to allocate new core scheduling domain for vCPU threads: %w", err)
}
return nil
}
// ConsoleLog returns all output sent to the instance's console's ring buffer since startup.
func (d *qemu) ConsoleLog() (string, error) {
// Setup a new operation.
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionConsoleRetrieve, []operationlock.Action{operationlock.ActionRestart, operationlock.ActionRestore, operationlock.ActionMigrate}, false, true)
if err != nil {
return "", err
}
// Only mark the operation as done if only processing the console retrieval.
if op.Action() == operationlock.ActionConsoleRetrieve {
defer op.Done(nil)
}
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return "", err
}
logString, err := monitor.RingbufRead("console")
if err != nil {
// If a VM was started by an older version of Incus which was then upgraded, its
// console device won't be a ring buffer. We don't want to cause an error in this
// case, so just return an empty string.
if errors.Is(err, qmp.ErrNotARingbuf) {
return "", nil
}
return "", err
}
// If we got data back, append it to the log file for this instance.
if logString != "" {
logFile, err := os.OpenFile(d.ConsoleBufferLogPath(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return "", err
}
defer logger.WarnOnError(logFile.Close, "Failed to close log file")
_, err = logFile.WriteString(logString)
if err != nil {
return "", err
}
}
// Read and return the complete log for this instance.
fullLog, err := os.ReadFile(d.ConsoleBufferLogPath())
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// If there's no log file yet, such as right at VM creation, return an empty string.
return "", nil
}
return "", err
}
return string(fullLog), nil
}
// consoleSwapRBWithSocket swaps the qemu backend for the instance's console to a unix socket.
func (d *qemu) consoleSwapRBWithSocket() error {
// This will wipe out anything in the existing ring buffer; save any buffered data to log file first.
_, err := d.ConsoleLog()
if err != nil {
return err
}
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
// Create the unix socket here, which will be passed via file descriptor to qemu.
d.consoleSocket, err = linux.ListenUnix(d.consolePath())
if err != nil {
return err
}
d.consoleSocketFile, err = d.consoleSocket.File()
if err != nil {
_ = d.consoleSocket.Close()
_ = os.Remove(d.consolePath())
return err
}
return monitor.ChardevChange("console", qmp.ChardevChangeInfo{Type: "socket", FDName: "consoleSocket", File: d.consoleSocketFile})
}
// consoleSwapSocketWithRB swaps the qemu backend for the instance's console to a ring buffer.
func (d *qemu) consoleSwapSocketWithRB() error {
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
defer func() {
// Clean up the old socket.
_ = d.consoleSocketFile.Close()
_ = d.consoleSocket.Close()
_ = os.Remove(d.consolePath())
}()
return monitor.ChardevChange("console", qmp.ChardevChangeInfo{Type: "ringbuf"})
}
// ConsoleScreenshot returns a screenshot of the current VGA console in PNG format.
func (d *qemu) ConsoleScreenshot(screenshotFile *os.File) error {
if !d.IsRunning() {
return errors.New("Instance is not running")
}
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
err = screenshotFile.Chown(int(d.state.OS.UnprivUID), -1)
if err != nil {
return fmt.Errorf("Failed to chown screenshot path: %w", err)
}
// Take the screenshot.
err = monitor.Screendump(screenshotFile.Name())
if err != nil {
return fmt.Errorf("Failed taking screenshot: %w", err)
}
return nil
}
// ReloadDevice triggers an empty Update call to the underlying device.
func (d *qemu) ReloadDevice(devName string) error {
dev, err := d.deviceLoad(d, devName, d.expandedDevices[devName], false)
if err != nil {
return err
}
return dev.Update(d.expandedDevices, true)
}
// DumpGuestMemory dumps the guest memory to a file in the specified format.
func (d *qemu) DumpGuestMemory(w *os.File, format string) error {
if !d.IsRunning() {
return errors.New("Instance is not running")
}
// Check if the agent is running.
monitor, err := d.qmpConnect()
if err != nil {
return err
}
defer monitor.Disconnect()
// Dump the guest memory.
err = monitor.SendFile("memory-dump", w)
if err != nil {
return err
}
err = monitor.DumpGuestMemory("memory-dump", format)
if err != nil {
return err
}
// Close the writer.
err = w.Close()
if err != nil {
return err
}
return nil
}
// CanLiveMigrate returns whether the VM is live-migratable.
func (d *qemu) CanLiveMigrate() bool {
// Refuse migration if not enabled.
if !util.IsTrue(d.expandedConfig["migration.stateful"]) {
return false
}
// Additional checks when the VM is running.
if d.IsRunning() {
// Check if it may have been enabled through a later profile (and so still not supported).
bs, err := d.getBootState()
if err != nil {
return false
}
// Ideally we'd just check for Version > 0 but that would prevent
// all migrations from older Incus versions that predate the introduction
// of the boot state recording. So instead rely on the machine type
// recording which is going to be present on those older versions too.
if bs.MachineType == "" {
return false
}
}
return true
}
// GuestOS returns the guest OS. In this driver, we consider anything unknown to be Linux.
func (d *qemu) GuestOS() osinfo.OSType {
osType, _ := osinfo.DetermineOS(strings.ToLower(d.expandedConfig["image.os"]))
return osType
}
// CreateQcow2Snapshot creates a qcow2 snapshot for a running instance.
func (d *qemu) CreateQcow2Snapshot(devPath string, devName string, snapshotName string, backingFilename string, stateful bool) error {
reverter := revert.New()
defer reverter.Fail()
monitor, err := d.qmpConnect()
if err != nil {
return err
}
f, err := os.OpenFile(devPath, unix.O_RDWR, 0)
if err != nil {
return fmt.Errorf("Failed opening file descriptor for disk device %s: %w", devPath, err)
}
defer logger.WarnOnError(f.Close, "Failed to close file")
devName = d.blockNodeName(linux.PathNameEncode(devName))
// Select all block devices related to a qcow2 backing chain.
blockDevs, err := d.fetchBlockDeviceChain(monitor, devName)
if err != nil {
return err
}
// Fetch the current maximum overlay index.
overlayNodeIndex := currentQcow2OverlayIndex(blockDevs, devName)
nextOverlayName := fmt.Sprintf("%s_overlay%d", devName, overlayNodeIndex+1)
currentRootNode := blockDevs[len(blockDevs)-1]
info, err := monitor.SendFileWithFDSet(nextOverlayName, f, false)
if err != nil {
return fmt.Errorf("Failed sending file descriptor of %q for disk device: %w", f.Name(), err)
}
reverter.Add(func() { _ = monitor.RemoveFDFromFDSet(nextOverlayName) })
blockDev := map[string]any{
"driver": "qcow2",
"discard": "unmap", // Forward as an unmap request. This is the same as `discard=on` in the qemu config file.
"node-name": nextOverlayName,
"read-only": false,
"file": map[string]any{
"driver": "host_device",
"filename": fmt.Sprintf("/dev/fdset/%d", info.ID),
},
}
// Add overlay block dev.
err = monitor.AddBlockDevice(blockDev, nil, false)
if err != nil {
return fmt.Errorf("Fail to add block device: %w", err)
}
reverter.Add(func() { _ = monitor.RemoveBlockDevice(nextOverlayName) })
// Take a snapshot of the root disk and redirect writes to the snapshot disk.
err = monitor.BlockDevSnapshot(currentRootNode, nextOverlayName)
if err != nil {
return fmt.Errorf("Failed taking storage snapshot: %w", err)
}
reverter.Add(func() { _ = monitor.BlockCommit(nextOverlayName, "", "") })
// Update metadata of the backing file.
// Use the Qcow2Rebase method when performing stateful snapshots.
// Using QMP to modify a volume that was added while the VM is paused can cause QEMU to crash.
if stateful {
err = storageDrivers.Qcow2Rebase(devPath, backingFilename)
if err != nil {
return err
}
} else {
err = monitor.ChangeBackingFile(nextOverlayName, nextOverlayName, backingFilename)
if err != nil {
return fmt.Errorf("Failed changing backing file: %w", err)
}
}
reverter.Success()
return nil
}
// fetchBlockDeviceChain returns the ordered list of block device names
// required to load a volume, including any dependent layers.
func (d *qemu) fetchBlockDeviceChain(m *qmp.Monitor, blockDevName string) ([]string, error) {
// Fetch information about block devices.
blockdevNames, err := m.QueryNamedBlockNodes()
if err != nil {
return nil, fmt.Errorf("Failed fetching block nodes names: %w", err)
}
return filterAndSortQcow2Blockdevs(blockdevNames, blockDevName), nil
}
// DeleteQcow2Snapshot deletes a qcow2 snapshot for a running instance.
func (d *qemu) DeleteQcow2Snapshot(devName string, snapshotIndex int, backingFilename string) error {
monitor, err := d.qmpConnect()
if err != nil {
return err
}
devName = d.blockNodeName(linux.PathNameEncode(devName))
// Select all block devices related to a qcow2 backing chain.
blockDevs, err := d.fetchBlockDeviceChain(monitor, devName)
if err != nil {
return err
}
d.logger.Debug("QCOW2 blockdev chain:", logger.Ctx{"blockdev": blockDevs, "snapshotIndex": snapshotIndex, "devName": devName})
if snapshotIndex < 0 || (snapshotIndex+1) >= len(blockDevs) {
return fmt.Errorf("Incorrect snapshot index: %d", snapshotIndex)
}
rootDevName := blockDevs[len(blockDevs)-1]
snapChildDevName := blockDevs[snapshotIndex+1]
snapDevName := blockDevs[snapshotIndex]
err = monitor.BlockCommit(rootDevName, snapChildDevName, snapDevName)
if err != nil {
return err
}
err = monitor.RemoveBlockDevice(snapChildDevName)
if err != nil {
d.logger.Error("Remove block device error", logger.Ctx{"err": err})
return err
}
err = monitor.RemoveFDFromFDSet(snapChildDevName)
if err != nil {
d.logger.Error("Remove fd from fd set", logger.Ctx{"err": err})
return err
}
if backingFilename == "" {
return nil
}
if snapshotIndex+2 >= len(blockDevs) {
return fmt.Errorf("Incorrect snapshot index for backing file update: %d", snapshotIndex)
}
// Update metadata of the backing file.
err = monitor.ChangeBackingFile(rootDevName, blockDevs[snapshotIndex+2], backingFilename)
if err != nil {
return fmt.Errorf("Failed changing backing file: %w", err)
}
return nil
}
// ExportQcow2Block exports a qcow2 block device by exposing it through a QEMU NBD server.
func (d *qemu) ExportQcow2Block(diskName string, blockIndex int) (func(), string, error) {
monitor, err := d.qmpConnect()
if err != nil {
return nil, "", err
}
socketPath := d.migrateSockPath()
// Cleanup any leftover sockets.
err = os.Remove(socketPath)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, "", fmt.Errorf("Failed to remove stale migration socket %q: %w", socketPath, err)
}
// Reference the socket through a short /proc path to handle run paths that
// exceed the unix socket path limit. The directory is kept open until cleanup
// so qemu-nbd can connect through the same reference.
runDir, err := os.OpenFile(d.RunPath(), unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return nil, "", err
}
reverter := revert.New()
defer reverter.Fail()
reverter.Add(func() { _ = runDir.Close() })
shortSocketPath := fmt.Sprintf("/proc/%d/fd/%d/%s", os.Getpid(), runDir.Fd(), filepath.Base(socketPath))
addr, err := net.ResolveUnixAddr("unix", shortSocketPath)
if err != nil {
return nil, "", err
}
migrationSock, err := net.ListenUnix("unix", addr)
if err != nil {
return nil, "", fmt.Errorf("Error connecting to migration socket %q: %w", socketPath, err)
}
migrationFile, err := migrationSock.File()
if err != nil {
return nil, "", fmt.Errorf("Error opening migration socket %q: %w", socketPath, err)
}
err = monitor.SendFile(socketPath, migrationFile)
if err != nil {
return nil, "", fmt.Errorf("Failed to send migration file descriptor: %w", err)
}
err = monitor.NBDUnixServerStart(socketPath)
if err != nil {
return nil, "", fmt.Errorf("Failed starting NBD server: %w", err)
}
escapedDeviceName := linux.PathNameEncode(diskName)
nodeName := d.blockNodeName(escapedDeviceName)
// Selects all block devices related to this instance (backing, root disk, overlays).
blockDevs, err := d.fetchBlockDeviceChain(monitor, nodeName)
if err != nil {
return nil, "", err
}
d.logger.Debug("Instance block devices:", logger.Ctx{"blockdev": blockDevs, "blockIndex": blockIndex})
if blockIndex < 0 || blockIndex >= len(blockDevs) {
return nil, "", fmt.Errorf("Incorrect block device index: %d", blockIndex)
}
exportBlockName := blockDevs[blockIndex]
exportDiskPath := fmt.Sprintf("nbd+unix:///%s?socket=%s", exportBlockName, shortSocketPath)
err = monitor.NBDBlockExportAdd(exportBlockName, exportBlockName, false, "", nil)
if err != nil {
return nil, "", fmt.Errorf("Failed adding disk to NBD server: %w", err)
}
reverter.Success()
return func() {
_ = monitor.NBDServerStop()
_ = migrationSock.Close()
_ = runDir.Close()
}, exportDiskPath, nil
}
func (d *qemu) isQCOW2(devPath string) (bool, error) {
imgInfo, err := storageDrivers.Qcow2Info(devPath)
if err != nil {
return false, err
}
return imgInfo.Format == storageDrivers.BlockVolumeTypeQcow2, nil
}
func (d *qemu) qcow2BlockDev(m *qmp.Monitor, nodeName string, aioMode string, directCache bool, noFlushCache bool, permissions int, readonly bool, backingPaths []string, iter int) (string, error) {
devName := backingPaths[0]
backingNodeName := fmt.Sprintf("%s_backing%d", nodeName, iter)
f, err := os.OpenFile(devName, permissions, 0)
if err != nil {
return "", fmt.Errorf("Failed opening file descriptor for disk device %q: %w", devName, err)
}
defer logger.WarnOnError(f.Close, "Failed to close file")
info, err := m.SendFileWithFDSet(backingNodeName, f, readonly)
if err != nil {
return "", fmt.Errorf("Failed sending file descriptor of %q for disk device %q: %w", f.Name(), devName, err)
}
blockDev := map[string]any{
"driver": "qcow2",
"discard": "unmap", // Forward as an unmap request. This is the same as `discard=on` in the qemu config file.
"node-name": backingNodeName,
"read-only": false,
"file": map[string]any{
"driver": "host_device",
"filename": fmt.Sprintf("/dev/fdset/%d", info.ID),
"aio": aioMode,
"cache": map[string]any{
"direct": directCache,
"no-flush": noFlushCache,
},
},
}
// If there are any children, load block information about them.
if len(backingPaths) > 1 {
parentNodeName, err := d.qcow2BlockDev(m, nodeName, aioMode, directCache, noFlushCache, permissions, readonly, backingPaths[1:], iter+1)
if err != nil {
return "", err
}
blockDev["backing"] = parentNodeName
}
err = m.AddBlockDevice(blockDev, nil, false)
if err != nil {
return "", err
}
return backingNodeName, nil
}
// currentQcow2OverlayIndex returns the current maximum overlay index.
func currentQcow2OverlayIndex(names []string, prefix string) int {
re := regexp.MustCompile(fmt.Sprintf(`^%s_overlay(\d+)$`, prefix))
maxIndex := -1
for _, name := range names {
m := re.FindStringSubmatch(name)
if len(m) == 2 {
n, err := strconv.Atoi(m[1])
if err == nil && n > maxIndex {
maxIndex = n
}
}
}
return maxIndex
}
func (d *qemu) needsFullRestart() bool {
// Check if we have a pending change.
if d.localConfig["volatile.vm.needs_reset"] != "" {
return true
}
// Check if the QEMU binary has changed.
pid, _ := d.pid()
if pid <= 0 {
return true
}
exePath, _, err := d.qemuArchConfig(d.architecture)
if err != nil {
return true
}
var curExe unix.Stat_t
err = unix.Stat(fmt.Sprintf("/proc/%d/exe", pid), &curExe)
if err != nil {
return true
}
var nextExe unix.Stat_t
err = unix.Stat(exePath, &nextExe)
if err != nil {
return true
}
if curExe.Size != nextExe.Size || curExe.Ino != nextExe.Ino || curExe.Dev != nextExe.Dev {
return true
}
// Full restart isn't required.
return false
}
// ConnectNBD exports a disk over NBD. Not supported by containers.
func (d *qemu) ConnectNBD(diskName string, volSize int64, writable bool) (net.Conn, func(), error) {
monitor, err := d.qmpConnect()
if err != nil {
return nil, nil, err
}
// Check for existing NBD block exports to detect if another operation is in progress.
blocks, err := monitor.QueryNBDBlockExports()
if err == nil && len(blocks) > 0 {
return nil, nil, fmt.Errorf("Another NBD operation is already in progress for: %s", blocks[0].NodeName)
}
nbdConn, err := monitor.NBDServerStart("", 1)
if err != nil {
return nil, nil, fmt.Errorf("Failed starting NBD server: %w", err)
}
d.logger.Debug("User requested NBD server started")
reverter := revert.New()
defer reverter.Fail()
overlayNode := ""
disconnect := func() {
d.logger.Debug("User requested NBD server stopped")
_ = nbdConn.Close()
_ = monitor.NBDServerStop()
if overlayNode != "" {
err := d.removeEphemeralOverlay(monitor, overlayNode)
if err != nil {
d.logger.Error("Failed removing temporary snapshot overlay", logger.Ctx{"overlay": overlayNode, "err": err})
}
}
}
reverter.Add(disconnect)
bitmaps, err := d.GetBitmaps(diskName)
if err != nil {
return nil, nil, fmt.Errorf("Failed fetching bitmaps for %q: %w", diskName, err)
}
bitmapNames := []string{}
for _, b := range bitmaps {
if b.Inconsistent {
continue
}
bitmapNames = append(bitmapNames, b.Name)
}
escapedDeviceName := linux.PathNameEncode(diskName)
nodeName := d.blockNodeName(escapedDeviceName)
blockDevs, err := d.fetchBlockDeviceChain(monitor, nodeName)
if err != nil {
return nil, nil, fmt.Errorf("Failed fetching disk chain: %w", err)
}
blockExport := blockDevs[len(blockDevs)-1]
exportNode := blockExport
if !writable {
// Expose a frozen view of the disk through a copy-before-write overlay
// (see ConnectNBDAllDisks).
snapNode, baseNode, removeOverlay, err := d.prepareEphemeralSnapshot(monitor, blockExport, volSize, true)
if err != nil {
return nil, nil, fmt.Errorf("Failed creating temporary snapshot: %w", err)
}
err = monitor.BlockDevBackupTransaction([]qmp.BlockDevBackupTarget{{Device: baseNode, Target: snapNode, Sync: "none", JobID: snapNode}})
if err != nil {
removeOverlay()
return nil, nil, fmt.Errorf("Failed creating temporary snapshot: %w", err)
}
overlayNode = snapNode
exportNode = snapNode
}
err = monitor.NBDBlockExportAdd(exportNode, "", writable, blockExport, bitmapNames)
if err != nil {
return nil, nil, fmt.Errorf("Failed adding disk to NBD server: %w", err)
}
cleanup := reverter.Clone().Fail
reverter.Success()
return nbdConn, cleanup, nil
}
// nbdSession tracks a running NBD session and its active connections.
type nbdSession struct {
connections int
stop func()
}
// nbdSessions tracks the active NBD sessions by instance ID.
var (
nbdSessionsMu sync.Mutex
nbdSessions = map[int]*nbdSession{}
)
// releaseNBDSession drops one connection from the instance's NBD session, stopping the session
// when no connections are left.
func (d *qemu) releaseNBDSession() {
nbdSessionsMu.Lock()
defer nbdSessionsMu.Unlock()
session := nbdSessions[d.id]
if session == nil {
return
}
session.connections--
if session.connections > 0 {
return
}
delete(nbdSessions, d.id)
session.stop()
}
// ConnectNBDAllDisks exports all of the instance's block disks read-only over a single NBD server.
// When reuse is true, an additional connection to an already running NBD server is returned instead.
// The NBD server is stopped once all of its connections are closed.
func (d *qemu) ConnectNBDAllDisks(reuse bool) (net.Conn, func(), error) {
monitor, err := d.qmpConnect()
if err != nil {
return nil, nil, err
}
// Connect to an already running NBD server when requested.
if reuse {
nbdSessionsMu.Lock()
session := nbdSessions[d.id]
if session == nil {
nbdSessionsMu.Unlock()
return nil, nil, errors.New("No NBD session is currently active")
}
conn, err := net.Dial("unix", d.nbdPath())
if err != nil {
nbdSessionsMu.Unlock()
return nil, nil, fmt.Errorf("Failed connecting to NBD server: %w", err)
}
session.connections++
nbdSessionsMu.Unlock()
cleanup := func() {
_ = conn.Close()
d.releaseNBDSession()
}
return conn, cleanup, nil
}
// Check for existing NBD block exports to detect if another operation is in progress.
blocks, err := monitor.QueryNBDBlockExports()
if err == nil && len(blocks) > 0 {
return nil, nil, fmt.Errorf("Another NBD operation is already in progress for: %s", blocks[0].NodeName)
}
// Determine the set of block-backed disks that can be exported.
namedNodes, err := monitor.QueryNamedBlockNodes()
if err != nil {
return nil, nil, fmt.Errorf("Failed fetching block nodes names: %w", err)
}
nodeSet := make(map[string]struct{}, len(namedNodes))
for _, n := range namedNodes {
nodeSet[n] = struct{}{}
}
deviceNames := []string{}
for devName, devConf := range d.ExpandedDevices() {
if devConf["type"] != "disk" {
continue
}
// Only block-backed disks have a matching QEMU block node, this filters out filesystem
// shares (e.g. virtiofs) which can't be exported over NBD.
nodeName := d.blockNodeName(linux.PathNameEncode(devName))
_, ok := nodeSet[nodeName]
if !ok {
continue
}
deviceNames = append(deviceNames, devName)
}
if len(deviceNames) == 0 {
return nil, nil, errors.New("Instance has no exportable disks")
}
// Export the disks in a stable order.
sort.Strings(deviceNames)
// Recover any overlay left behind by a previously failed teardown so the
// disks can be snapshotted and exported again.
for _, devName := range deviceNames {
overlayNode := ephemeralSnapshotName(d.blockNodeName(linux.PathNameEncode(devName)))
_, ok := nodeSet[overlayNode]
if !ok {
continue
}
err = d.removeEphemeralOverlay(monitor, overlayNode)
if err != nil {
return nil, nil, fmt.Errorf("Failed recovering disk %q from an earlier failed teardown: %w", devName, err)
}
}
nbdConn, err := monitor.NBDServerStart(d.nbdPath(), 0)
if err != nil {
return nil, nil, fmt.Errorf("Failed starting NBD server: %w", err)
}
d.logger.Debug("User requested NBD server started")
reverter := revert.New()
defer reverter.Fail()
reverter.Add(func() {
d.logger.Debug("User requested NBD server stopped")
_ = nbdConn.Close()
err := monitor.NBDServerStop()
if err != nil {
d.logger.Error("Failed stopping NBD server", logger.Ctx{"err": err})
}
_ = os.Remove(d.nbdPath())
})
type exportTarget struct {
deviceName string
exportNode string
bitmapNode string
bitmaps []string
}
targets := make([]exportTarget, 0, len(deviceNames))
backups := make([]qmp.BlockDevBackupTarget, 0, len(deviceNames))
overlays := make([]string, 0, len(deviceNames))
// Prepare a copy-before-write overlay for each disk, exposing a frozen view of it while the
// guest keeps writing to the disk itself. Snapshotting the disk instead would reopen it and
// its persistent dirty bitmaps read-only, making the eventual merge of the snapshot fail.
for _, devName := range deviceNames {
nodeName := d.blockNodeName(linux.PathNameEncode(devName))
bitmaps, err := d.GetBitmaps(devName)
if err != nil {
return nil, nil, fmt.Errorf("Failed fetching bitmaps for %q: %w", devName, err)
}
bitmapNames := []string{}
for _, b := range bitmaps {
if b.Inconsistent {
continue
}
bitmapNames = append(bitmapNames, b.Name)
}
diskSize, err := monitor.BlockNodeSize(nodeName)
if err != nil {
return nil, nil, fmt.Errorf("Failed fetching size for %q: %w", devName, err)
}
overlayNode, baseNode, removeOverlay, err := d.prepareEphemeralSnapshot(monitor, nodeName, diskSize, true)
if err != nil {
return nil, nil, fmt.Errorf("Failed creating temporary snapshot for %q: %w", devName, err)
}
reverter.Add(removeOverlay)
backups = append(backups, qmp.BlockDevBackupTarget{Device: baseNode, Target: overlayNode, Sync: "none", JobID: overlayNode})
targets = append(targets, exportTarget{deviceName: devName, exportNode: overlayNode, bitmapNode: baseNode, bitmaps: bitmapNames})
overlays = append(overlays, overlayNode)
}
// Start all copy-before-write jobs atomically so the exported disks share a consistent
// point in time.
err = monitor.BlockDevBackupTransaction(backups)
if err != nil {
return nil, nil, fmt.Errorf("Failed creating consistent storage snapshot: %w", err)
}
// The overlays never hold guest writes, teardown cancels the jobs and drops them.
reverter.Success()
stop := func() {
d.logger.Debug("User requested NBD server stopped")
_ = nbdConn.Close()
err := monitor.NBDServerStop()
if err != nil {
d.logger.Error("Failed stopping NBD server", logger.Ctx{"err": err})
}
_ = os.Remove(d.nbdPath())
for _, overlayNode := range overlays {
err := d.removeEphemeralOverlay(monitor, overlayNode)
if err != nil {
d.logger.Error("Failed removing temporary snapshot overlay", logger.Ctx{"overlay": overlayNode, "err": err})
}
}
}
// Add an NBD export per disk, using the Incus device name as the export name.
for _, target := range targets {
err = monitor.NBDBlockExportAdd(target.exportNode, target.deviceName, false, target.bitmapNode, target.bitmaps)
if err != nil {
stop()
return nil, nil, fmt.Errorf("Failed adding disk %q to NBD server: %w", target.deviceName, err)
}
}
// Register the session, it's stopped once its last connection is released.
nbdSessionsMu.Lock()
nbdSessions[d.id] = &nbdSession{connections: 1, stop: stop}
nbdSessionsMu.Unlock()
return nbdConn, d.releaseNBDSession, nil
}
// CreateBitmap creates a dirty bitmap.
func (d *qemu) CreateBitmap(deviceNames []string, data api.StorageVolumeBitmapsPost) error {
monitor, err := d.qmpConnect()
if err != nil {
return err
}
blockNames := []string{}
for _, devName := range deviceNames {
escapedDeviceName := linux.PathNameEncode(devName)
nodeName := d.blockNodeName(escapedDeviceName)
blockDevs, err := d.fetchBlockDeviceChain(monitor, nodeName)
if err != nil {
return fmt.Errorf("Failed fetching disk chain: %w", err)
}
blockNames = append(blockNames, blockDevs[len(blockDevs)-1])
}
err = monitor.AddDirtyBitmap(blockNames, data.Name, data.Granularity, data.Persistent, data.Disabled)
if err != nil {
return err
}
return nil
}
// DeleteBitmap deletes a dirty bitmap.
func (d *qemu) DeleteBitmap(deviceName string, bitmapName string) error {
monitor, err := d.qmpConnect()
if err != nil {
return err
}
escapedDeviceName := linux.PathNameEncode(deviceName)
nodeName := d.blockNodeName(escapedDeviceName)
blockDevs, err := d.fetchBlockDeviceChain(monitor, nodeName)
if err != nil {
return fmt.Errorf("Failed fetching disk chain: %w", err)
}
blockName := blockDevs[len(blockDevs)-1]
err = monitor.RemoveDirtyBitmap(blockName, bitmapName)
if err != nil {
return err
}
return nil
}
// GetBitmaps fetches dirty bitmaps.
func (d *qemu) GetBitmaps(deviceName string) ([]api.StorageVolumeBitmap, error) {
monitor, err := d.qmpConnect()
if err != nil {
return nil, err
}
escapedDeviceName := linux.PathNameEncode(deviceName)
nodeName := d.blockNodeName(escapedDeviceName)
blockDevs, err := d.fetchBlockDeviceChain(monitor, nodeName)
if err != nil {
return nil, fmt.Errorf("Failed fetching disk chain: %w", err)
}
blockName := blockDevs[len(blockDevs)-1]
blocks, err := monitor.QueryBlock()
if err != nil {
return nil, err
}
result := []api.StorageVolumeBitmap{}
for _, block := range blocks {
if block.Inserted.NodeName == blockName {
for _, bitmap := range block.Inserted.DirtyBitmaps {
result = append(result, api.StorageVolumeBitmap{
Name: bitmap.Name,
Count: bitmap.Count,
Granularity: bitmap.Granularity,
Recording: bitmap.Recording,
Busy: bitmap.Busy,
Persistent: bitmap.Persistent,
Inconsistent: bitmap.Inconsistent,
})
}
return result, nil
}
}
return nil, fmt.Errorf("Requested device not found")
}
// selinuxEnsureContext generates and persists the SELinux context for this instance.
func (d *qemu) selinuxEnsureContext() (bool, error) {
if !d.state.OS.SELinuxEnabled {
return false, nil
}
previousCtx := d.localConfig["volatile.selinux.context"]
allocLevel := func() (string, func(), error) {
used, err := d.selinuxCollectUsedLevels()
if err != nil {
return "", nil, err
}
return selinux.AllocateLevel(used)
}
ctx, needsPersist, release, err := selinux.InstanceContext(d.state.OS, instancetype.VM, d.localConfig, d.expandedConfig, allocLevel)
if err != nil {
return false, err
}
defer release()
if ctx == "" {
return false, nil
}
if needsPersist {
err = d.VolatileSet(map[string]string{"volatile.selinux.context": ctx})
if err != nil {
return false, fmt.Errorf("Failed to persist SELinux context: %w", err)
}
}
// Return true if this is the first time a context was generated.
return previousCtx == "", nil
}
// selinuxLabelFiles applies SELinux file labels to the VM instance directory.
func (d *qemu) selinuxLabelFiles(contextIsNew bool) error {
if !d.state.OS.SELinuxEnabled {
return nil
}
ctx := d.localConfig["volatile.selinux.context"]
if ctx == "" {
return nil
}
if !contextIsNew {
if d.localConfig["security.selinux.level"] != "" {
return nil
}
}
fileCtx := selinux.InstanceFileContext(ctx, instancetype.VM, d.expandedConfig)
if fileCtx == "" {
return fmt.Errorf("Failed to derive file context from %q", ctx)
}
return selinux.LabelTree(d.Path(), fileCtx, "")
}
// buildDataFileInfo builds the "data-file" field for block device options.
func buildDataFileInfo(nodeName string, m *qmp.Monitor, driveConf deviceConfig.MountEntryItem, permissions int, readonly bool, aioMode string, directCache bool, noFlushCache bool) (map[string]any, error) {
if driveConf.DataFilePath == "" {
return nil, nil
}
if !util.PathExists(driveConf.DataFilePath) {
return nil, fmt.Errorf("qcow2 data-file %q does not exist", driveConf.DataFilePath)
}
df, err := os.OpenFile(driveConf.DataFilePath, permissions, 0)
if err != nil {
return nil, fmt.Errorf("Failed opening qcow2 data-file %q: %w", driveConf.DataFilePath, err)
}
defer func() { _ = df.Close() }()
dataInfo, err := m.SendFileWithFDSet(nodeName+"_raw", df, readonly)
if err != nil {
return nil, fmt.Errorf("Failed sending qcow2 data file descriptor %q: %w", driveConf.DataFilePath, err)
}
reverter := revert.New()
defer reverter.Fail()
reverter.Add(func() {
_ = m.RemoveFDFromFDSet(nodeName + "_raw")
})
dataDev := map[string]any{
"filename": fmt.Sprintf("/dev/fdset/%d", dataInfo.ID),
"aio": aioMode,
"cache": map[string]any{
"direct": directCache,
"no-flush": noFlushCache,
},
}
dataFilePathInfo, err := os.Stat(driveConf.DataFilePath)
if err != nil {
return nil, fmt.Errorf("Invalid data-file path %q: %w", driveConf.DataFilePath, err)
}
if linux.IsBlockdev(dataFilePathInfo.Mode()) {
dataDev["driver"] = "host_device"
} else {
dataDev["driver"] = "file"
}
reverter.Success()
return dataDev, nil
}
// GetNVRAM gets the NVRAM.
func (d *qemu) GetNVRAM() (*uefi.Store, error) {
if !d.IsRunning() {
// Mount the instance's config volume.
_, err := d.mount()
if err != nil {
return nil, err
}
defer logger.WarnOnError(d.unmount, "Failed to unmount instance")
_, err = os.Stat(d.nvramPath())
if errors.Is(err, os.ErrNotExist) {
// The NVRAM hasnt been initialized yet.
err = d.setupNvram()
if err != nil {
return nil, err
}
}
}
nvRAM, err := os.ReadFile(d.nvramPath())
if err != nil {
return nil, fmt.Errorf("Failed opening NVRAM file: %w", err)
}
return uefi.ParseNVRAM(nvRAM)
}
// SetNVRAM sets the NVRAM.
func (d *qemu) SetNVRAM(store *uefi.Store) error {
// Mount the instance's config volume.
_, err := d.mount()
if err != nil {
return err
}
defer logger.WarnOnError(d.unmount, "Failed to unmount instance")
_, err = os.Stat(d.nvramPath())
if errors.Is(err, os.ErrNotExist) {
// The NVRAM hasnt been initialized yet.
err = d.setupNvram()
if err != nil {
return err
}
}
f, err := os.Create(d.nvramPath())
if err != nil {
return fmt.Errorf("Failed opening NVRAM file: %w", err)
}
b, err := store.Bytes()
if err != nil {
return err
}
_, err = f.Write(b)
return err
}