Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/config_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5648,6 +5648,13 @@ This value is required by some providers.

<!-- config group storage_linstor-common end -->
<!-- config group storage_lvm-common start -->
```{config:option} block.type storage_lvm-common
:condition: "block-based volume"
:default: "same as `volume.block.type`"
:shortdesc: "Type of the block volume"

```

```{config:option} lvm.metadata_size storage_lvm-common
:default: "`0` (auto)"
:scope: "global"
Expand Down
21 changes: 11 additions & 10 deletions internal/server/device/config/device_runconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ type RunConfigItem struct {

// MountEntryItem represents a single mount entry item.
type MountEntryItem struct {
DevName string // The internal name for the device.
DevPath string // Describes the block special device or remote filesystem to be mounted.
TargetPath string // Describes the mount point (target) for the filesystem.
FSType string // Describes the type of the filesystem.
Opts []string // Describes the mount options associated with the filesystem.
Freq int // Used by dump(8) to determine which filesystems need to be dumped. Defaults to zero (don't dump) if not present.
PassNo int // Used by fsck(8) to determine the order in which filesystem checks are done at boot time. Defaults to zero (don't fsck) if not present.
OwnerShift string // Ownership shifting mode, use constants MountOwnerShiftNone, MountOwnerShiftStatic or MountOwnerShiftDynamic.
Limits *DiskLimits // Disk limits.
Size int64 // Expected disk size in bytes.
DevName string // The internal name for the device.
DevPath string // Describes the block special device or remote filesystem to be mounted.
BackingPath []string // Describes the block special device to be mounted as backing drive for qcow2.
TargetPath string // Describes the mount point (target) for the filesystem.
FSType string // Describes the type of the filesystem.
Opts []string // Describes the mount options associated with the filesystem.
Freq int // Used by dump(8) to determine which filesystems need to be dumped. Defaults to zero (don't dump) if not present.
PassNo int // Used by fsck(8) to determine the order in which filesystem checks are done at boot time. Defaults to zero (don't fsck) if not present.
OwnerShift string // Ownership shifting mode, use constants MountOwnerShiftNone, MountOwnerShiftStatic or MountOwnerShiftDynamic.
Limits *DiskLimits // Disk limits.
Size int64 // Expected disk size in bytes.
}

// RootFSEntryItem represents the root filesystem options for an Instance.
Expand Down
5 changes: 5 additions & 0 deletions internal/server/instance/drivers/driver_lxc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9379,3 +9379,8 @@ func (d *lxc) setupCredentials(update bool) error {
func (d *lxc) GuestOS() string {
return "linux"
}

// CreateQcow2Snapshot creates a qcow2 snapshot for a running instance. Not supported by containers.
func (d *lxc) CreateQcow2Snapshot(snapName string, backingFilename string) error {
return nil
}
214 changes: 207 additions & 7 deletions internal/server/instance/drivers/driver_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"sort"
"strconv"
Expand Down Expand Up @@ -4337,11 +4338,12 @@ func (d *qemu) addRootDriveConfig(qemuDev map[string]any, mountInfo *storagePool

// Generate a new device config with the root device path expanded.
driveConf := deviceConfig.MountEntryItem{
DevName: rootDriveConf.DevName,
DevPath: mountInfo.DiskPath,
Opts: rootDriveConf.Opts,
TargetPath: rootDriveConf.TargetPath,
Limits: rootDriveConf.Limits,
DevName: rootDriveConf.DevName,
DevPath: mountInfo.DiskPath,
BackingPath: mountInfo.BackingPath,
Opts: rootDriveConf.Opts,
TargetPath: rootDriveConf.TargetPath,
Limits: rootDriveConf.Limits,
}

if d.storagePool.Driver().Info().Remote {
Expand Down Expand Up @@ -4540,13 +4542,14 @@ func (d *qemu) addDriveConfig(qemuDev map[string]any, bootIndexes map[string]int
}

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.
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)) {
Expand Down Expand Up @@ -4852,7 +4855,40 @@ func (d *qemu) addDriveConfig(qemuDev map[string]any, bootIndexes map[string]int
_ = m.RemoveFDFromFDSet(nodeName)
})

blockDev["filename"] = fmt.Sprintf("/dev/fdset/%d", info.ID)
isQcow2, err := d.isQCOW2(srcDevPath)
if err != nil {
return fmt.Errorf("Failed checking disk format: %w", err)
}

if isQcow2 {
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": 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(driveConf.BackingPath) > 0 {
backingBlockDev, err := d.qcow2BlockDev(m, nodeName, aioMode, directCache, noFlushCache, permissions, readonly, driveConf.BackingPath, 0)
if err != nil {
return nil
}

blockDev["backing"] = backingBlockDev
}
} else {
blockDev["filename"] = fmt.Sprintf("/dev/fdset/%d", info.ID)
}
}

err := m.AddBlockDevice(blockDev, qemuDev, bus == "usb")
Expand Down Expand Up @@ -10419,3 +10455,167 @@ func (d *qemu) GuestOS() string {

return "unknown"
}

// CreateQcow2Snapshot creates a qcow2 snapshot for a running instance.
func (d *qemu) CreateQcow2Snapshot(snapshotName string, backingFilename string) error {
monitor, err := d.qmpConnect()
if err != nil {
return err
}

snap, err := instance.LoadByProjectAndName(d.state, d.project.Name, fmt.Sprintf("%s/%s", d.name, snapshotName))
if err != nil {
return fmt.Errorf("Load by project and name: %w", err)
}

pool, err := storagePools.LoadByInstance(d.state, snap)
if err != nil {
return fmt.Errorf("Load by instance: %w", err)
}

mountInfoRoot, err := pool.MountInstance(d, d.op)
if err != nil {
return fmt.Errorf("Mount instance: %w", err)
}

defer func() { _ = pool.UnmountInstance(d, d.op) }()

f, err := os.OpenFile(mountInfoRoot.DiskPath, unix.O_RDWR, 0)
if err != nil {
return fmt.Errorf("Failed opening file descriptor for disk device %s: %w", mountInfoRoot.DiskPath, err)
}

defer func() { _ = f.Close() }()

// Fetch information about block devices.
blockdevNames, err := monitor.QueryNamedBlockNodes()
if err != nil {
return fmt.Errorf("Failed fetching block nodes names: %w", err)
}

rootDevName, _, err := internalInstance.GetRootDiskDevice(d.expandedDevices.CloneNative())
if err != nil {
return fmt.Errorf("Failed getting instance root disk: %w", err)
}

escapedDeviceName := linux.PathNameEncode(rootDevName)
rootNodeName := d.blockNodeName(escapedDeviceName)

// Fetch the current maximum overlay index.
overlayNodeIndex := currentQcow2OverlayIndex(blockdevNames, rootNodeName)
nextOverlayName := fmt.Sprintf("%s_overlay%d", rootNodeName, overlayNodeIndex+1)

currentOverlayName := rootNodeName
if overlayNodeIndex >= 0 {
currentOverlayName = fmt.Sprintf("%s_overlay%d", rootNodeName, overlayNodeIndex)
}

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)
}

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)
}

// Take a snapshot of the root disk and redirect writes to the snapshot disk.
err = monitor.BlockDevSnapshot(currentOverlayName, nextOverlayName)
if err != nil {
return fmt.Errorf("Failed taking storage snapshot: %w", err)
}

// Update metadata of the backing file.
err = monitor.ChangeBackingFile(nextOverlayName, backingFilename)
if err != nil {
return fmt.Errorf("Failed changing backing file: %w", err)
}

return 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) (map[string]any, error) {
devName := backingPaths[0]
backingNodeName := fmt.Sprintf("%s_backing%d", nodeName, iter)

f, err := os.OpenFile(devName, permissions, 0)
if err != nil {
return nil, fmt.Errorf("Failed opening file descriptor for disk device %q: %w", devName, err)
}

defer func() { _ = f.Close() }()

info, err := m.SendFileWithFDSet(backingNodeName, f, readonly)
if err != nil {
return nil, 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 {
backingBlockDev, err := d.qcow2BlockDev(m, nodeName, aioMode, directCache, noFlushCache, permissions, readonly, backingPaths[1:], iter+1)
if err != nil {
return nil, err
}

blockDev["backing"] = backingBlockDev
}

return blockDev, 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
}
41 changes: 41 additions & 0 deletions internal/server/instance/drivers/qmp/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,47 @@ func (m *Monitor) NBDBlockExportAdd(deviceNodeName string) error {
return nil
}

// QueryNamedBlockNodes returns block nodes names.
func (m *Monitor) QueryNamedBlockNodes() ([]string, error) {
var resp struct {
Return []struct {
NodeName string `json:"node-name"`
} `json:"return"`
}

err := m.Run("query-named-block-nodes", nil, &resp)
if err != nil {
return nil, err
}

result := []string{}
for _, r := range resp.Return {
result = append(result, r.NodeName)
}

return result, nil
}

// ChangeBackingFile changes backing file name for node.
func (m *Monitor) ChangeBackingFile(nodeName string, backingFilename string) error {
var args struct {
Device string `json:"device"`
NodeName string `json:"image-node-name"`
BackingFile string `json:"backing-file"`
}

args.Device = nodeName
args.NodeName = nodeName
args.BackingFile = backingFilename

err := m.Run("change-backing-file", args, nil)
if err != nil {
return err
}

return nil
}

// BlockDevSnapshot creates a snapshot of a device using the specified snapshot device.
func (m *Monitor) BlockDevSnapshot(deviceNodeName string, snapshotNodeName string) error {
var args struct {
Expand Down
1 change: 1 addition & 0 deletions internal/server/instance/instance_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ type Instance interface {
Backups() ([]backup.InstanceBackup, error)
UpdateBackupFile() error
CanLiveMigrate() bool
CreateQcow2Snapshot(snapshotName string, backingFilename string) error

// Config handling.
Rename(newName string, applyTemplateTrigger bool) error
Expand Down
8 changes: 8 additions & 0 deletions internal/server/metadata/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -6410,6 +6410,14 @@
"storage_lvm": {
"common": {
"keys": [
{
"block.type": {
"condition": "block-based volume",
"default": "same as `volume.block.type`",
"longdesc": "",
"shortdesc": "Type of the block volume"
}
},
{
"lvm.metadata_size": {
"default": "`0` (auto)",
Expand Down
Loading
Loading