incus-mirror/cmd/incusd/daemon_storage.go
Benjamin Somers 94d9118a8b incusd/storage: Prevent creating daemon volumes on shared pools
Signed-off-by: Benjamin Somers <benjamin.somers@imt-atlantique.fr>
2026-05-28 10:15:02 +00:00

525 lines
14 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/lxc/incus/v7/internal/rsync"
"github.com/lxc/incus/v7/internal/server/db"
"github.com/lxc/incus/v7/internal/server/instance"
"github.com/lxc/incus/v7/internal/server/instance/instancetype"
"github.com/lxc/incus/v7/internal/server/node"
"github.com/lxc/incus/v7/internal/server/project"
"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"
internalUtil "github.com/lxc/incus/v7/internal/util"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/util"
)
func daemonStorageVolumesUnmount(s *state.State) error {
var storageBackups string
var storageImages string
err := s.DB.Node.Transaction(context.Background(), func(ctx context.Context, tx *db.NodeTx) error {
nodeConfig, err := node.ConfigLoad(ctx, tx)
if err != nil {
return err
}
storageBackups = nodeConfig.StorageBackupsVolume()
storageImages = nodeConfig.StorageImagesVolume()
return nil
})
if err != nil {
return err
}
unmount := func(storageType string, source string) error {
// Parse the source.
poolName, volumeName, err := daemonStorageSplitVolume(source)
if err != nil {
return err
}
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return err
}
// Mount volume.
_, err = pool.UnmountCustomVolume(api.ProjectDefaultName, volumeName, nil)
if err != nil {
return fmt.Errorf("Failed to unmount storage volume %q: %w", source, err)
}
return nil
}
if storageBackups != "" {
err := unmount("backups", storageBackups)
if err != nil {
return fmt.Errorf("Failed to unmount backups storage: %w", err)
}
}
if storageImages != "" {
err := unmount("images", storageImages)
if err != nil {
return fmt.Errorf("Failed to unmount images storage: %w", err)
}
}
return nil
}
func daemonStorageMount(s *state.State) error {
var storageBackups string
var storageImages string
var storageLogs string
err := s.DB.Node.Transaction(context.Background(), func(ctx context.Context, tx *db.NodeTx) error {
nodeConfig, err := node.ConfigLoad(ctx, tx)
if err != nil {
return err
}
storageBackups = nodeConfig.StorageBackupsVolume()
storageImages = nodeConfig.StorageImagesVolume()
storageLogs = nodeConfig.StorageLogsVolume()
return nil
})
if err != nil {
return err
}
mount := func(storageType string, source string) error {
// Parse the source.
poolName, volumeName, err := daemonStorageSplitVolume(source)
if err != nil {
return err
}
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return err
}
// Mount volume.
_, err = pool.MountCustomVolume(api.ProjectDefaultName, volumeName, nil)
if err != nil {
return fmt.Errorf("Failed to mount storage volume %q: %w", source, err)
}
// Ensure we have the correct symlink in place.
volStorageName := project.StorageVolume(api.ProjectDefaultName, volumeName)
_ = os.RemoveAll(internalUtil.VarPath(storageType))
err = os.Symlink(internalUtil.VarPath("storage-pools", poolName, "custom", volStorageName), internalUtil.VarPath(storageType))
if err != nil {
return fmt.Errorf("Failed to set up symlink for %q: %w", storageType, err)
}
return nil
}
if storageBackups != "" {
err := mount("backups", storageBackups)
if err != nil {
return fmt.Errorf("Failed to mount backups storage: %w", err)
}
}
if storageImages != "" {
err := mount("images", storageImages)
if err != nil {
return fmt.Errorf("Failed to mount images storage: %w", err)
}
}
if storageLogs != "" {
err := mount("logs", storageLogs)
if err != nil {
return fmt.Errorf("Failed to mount logs storage: %w", err)
}
}
return nil
}
func daemonStorageSplitVolume(volume string) (string, string, error) {
fields := strings.Split(volume, "/")
if len(fields) != 2 {
return "", "", errors.New("Invalid syntax for volume, must be <pool>/<volume>")
}
poolName := fields[0]
volumeName := fields[1]
return poolName, volumeName, nil
}
func daemonStorageValidate(s *state.State, storageType string, target string) error {
// Check syntax.
if target == "" {
return nil
}
poolName, volumeName, err := daemonStorageSplitVolume(target)
if err != nil {
return err
}
var poolID int64
var snapshots []db.StorageVolumeArgs
err = s.DB.Cluster.Transaction(context.Background(), func(ctx context.Context, tx *db.ClusterTx) error {
// Validate pool exists.
var pool *api.StoragePool
poolID, pool, _, err = tx.GetStoragePool(ctx, poolName)
if err != nil {
return fmt.Errorf("Unable to load storage pool %q: %w", poolName, err)
}
if slices.Contains(db.StorageRemoteDriverNames(), pool.Driver) {
return errors.New("Daemon storage volumes cannot be located on clustered storage pools")
}
// Confirm volume exists.
dbVol, err := tx.GetStoragePoolVolume(ctx, poolID, api.ProjectDefaultName, db.StoragePoolVolumeTypeCustom, volumeName, true)
if err != nil {
return fmt.Errorf("Failed loading storage volume %q in %q project: %w", target, api.ProjectDefaultName, err)
}
if dbVol.ContentType != db.StoragePoolVolumeContentTypeNameFS {
return fmt.Errorf("Storage volume %q in %q project is not filesystem content type", target, api.ProjectDefaultName)
}
snapshots, err = tx.GetLocalStoragePoolVolumeSnapshotsWithType(ctx, api.ProjectDefaultName, volumeName, db.StoragePoolVolumeTypeCustom, poolID)
if err != nil {
return fmt.Errorf("Unable to load storage volume snapshots %q in %q project: %w", target, api.ProjectDefaultName, err)
}
return nil
})
if err != nil {
return err
}
if len(snapshots) != 0 {
return errors.New("Storage volumes for use by Incus itself cannot have snapshots")
}
// If logs, ensure no running instances.
if storageType == "logs" {
localInstances, err := instance.LoadNodeAll(s, instancetype.Any)
if err != nil {
return fmt.Errorf("Failed loading local instance list: %w", err)
}
for _, inst := range localInstances {
if inst.IsRunning() {
return fmt.Errorf("`storage.logs_volume` cannot be changed if there are running instances")
}
}
}
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return err
}
// Mount volume.
_, err = pool.MountCustomVolume(api.ProjectDefaultName, volumeName, nil)
if err != nil {
return fmt.Errorf("Failed to mount storage volume %q: %w", target, err)
}
defer func() { _, _ = pool.UnmountCustomVolume(api.ProjectDefaultName, volumeName, nil) }()
// Validate volume is empty (ignore lost+found).
volStorageName := project.StorageVolume(api.ProjectDefaultName, volumeName)
mountpoint := storageDrivers.GetVolumeMountPath(poolName, storageDrivers.VolumeTypeCustom, volStorageName)
entries, err := os.ReadDir(mountpoint)
if err != nil {
return fmt.Errorf("Failed to list %q: %w", mountpoint, err)
}
for _, entry := range entries {
entryName := entry.Name()
// Don't fail on clean ext4 volumes.
if entryName == "lost+found" {
continue
}
// Don't fail on systems with snapdir=visible.
if entryName == ".zfs" {
continue
}
return fmt.Errorf("Storage volume %q isn't empty", target)
}
return nil
}
func daemonStorageMove(s *state.State, storageType string, target string) error {
var destPath string
isLogs := storageType == "logs"
if isLogs && target == "" && os.Getenv("INCUS_DIR") == "" {
// We keep the system-wide location when dealing with logs without a custom daemon path.
destPath = "/var/log/incus"
} else {
destPath = internalUtil.VarPath(storageType)
}
// Track down the current storage.
var sourcePool string
var sourceVolume string
sourcePath, err := os.Readlink(internalUtil.VarPath(storageType))
if err != nil {
sourcePath = destPath
} else {
fields := strings.Split(sourcePath, "/")
sourcePool = fields[len(fields)-3]
sourceVolume = fields[len(fields)-1]
}
moveContent := func(source string, target string) error {
// Copy the content.
_, err := rsync.LocalCopy(source, target, "", false)
if err != nil {
return err
}
// Remove the source content.
entries, err := os.ReadDir(source)
if err != nil {
return err
}
for _, entry := range entries {
err := os.RemoveAll(filepath.Join(source, entry.Name()))
if err != nil {
return err
}
}
return nil
}
// We should not move all data away from /var/log/incus and move it into the volume as that would interfere
// with /var/log/incus/incusd.log which is created prior to us setting up volume mounts
moveInstanceDirs := func(source string, target string) error {
entries, err := os.ReadDir(source)
if err != nil {
return err
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
src := filepath.Join(source, entry.Name())
dst := filepath.Join(target, entry.Name())
_, err := rsync.LocalCopy(src, dst, "", false)
if err != nil {
return err
}
err = os.RemoveAll(src)
if err != nil {
return err
}
}
return nil
}
// Deal with unsetting.
if target == "" {
// Things already look correct.
if sourcePath == destPath {
return nil
}
// Remove the symlink.
err = os.Remove(internalUtil.VarPath(storageType))
if err != nil {
return fmt.Errorf("Failed to delete storage symlink at %q: %w", destPath, err)
}
// Re-create as a directory.
// In the context of Logs, we ensure system log dir exists and move instance dirs back there
err = os.MkdirAll(destPath, 0o700)
if err != nil {
return fmt.Errorf("Failed to create directory %q: %w", destPath, err)
}
if isLogs {
err = moveInstanceDirs(sourcePath, destPath)
if err != nil {
return fmt.Errorf("Failed to move instance logs back to %q: %w", destPath, err)
}
} else {
// Move the data across.
err = moveContent(sourcePath, destPath)
if err != nil {
return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
}
}
pool, err := storagePools.LoadByName(s, sourcePool)
if err != nil {
return err
}
// Unmount old volume.
projectName, sourceVolumeName := project.StorageVolumeParts(sourceVolume)
_, err = pool.UnmountCustomVolume(projectName, sourceVolumeName, nil)
if err != nil {
if !isLogs {
return fmt.Errorf(`Failed to umount storage volume "%s/%s": %w`, sourcePool, sourceVolumeName, err)
}
logger.Warn("Unable to unmount logs storage, daemon restart required")
}
return nil
}
// Parse the target.
poolName, volumeName, err := daemonStorageSplitVolume(target)
if err != nil {
return err
}
pool, err := storagePools.LoadByName(s, poolName)
if err != nil {
return err
}
// Mount volume.
_, err = pool.MountCustomVolume(api.ProjectDefaultName, volumeName, nil)
if err != nil {
return fmt.Errorf("Failed to mount storage volume %q: %w", target, err)
}
// Set ownership & mode.
volStorageName := project.StorageVolume(api.ProjectDefaultName, volumeName)
mountpoint := storageDrivers.GetVolumeMountPath(poolName, storageDrivers.VolumeTypeCustom, volStorageName)
destPath = mountpoint
err = os.Chmod(mountpoint, 0o700)
if err != nil {
return fmt.Errorf("Failed to set permissions on %q: %w", mountpoint, err)
}
err = os.Chown(mountpoint, 0, 0)
if err != nil {
return fmt.Errorf("Failed to set ownership on %q: %w", mountpoint, err)
}
// Handle changes.
if sourcePath != internalUtil.VarPath(storageType) {
// Remove the symlink.
err := os.Remove(internalUtil.VarPath(storageType))
if err != nil {
return fmt.Errorf("Failed to remove the new symlink at %q: %w", internalUtil.VarPath(storageType), err)
}
// Create the new symlink.
err = os.Symlink(destPath, internalUtil.VarPath(storageType))
if err != nil {
return fmt.Errorf("Failed to create the new symlink at %q: %w", internalUtil.VarPath(storageType), err)
}
// Move the data across.
if isLogs {
err = moveInstanceDirs(sourcePath, destPath)
if err != nil {
return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
}
} else {
err = moveContent(sourcePath, destPath)
if err != nil {
return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
}
}
pool, err := storagePools.LoadByName(s, sourcePool)
if err != nil {
return err
}
// Unmount old volume.
projectName, sourceVolumeName := project.StorageVolumeParts(sourceVolume)
_, err = pool.UnmountCustomVolume(projectName, sourceVolumeName, nil)
if err != nil {
if !isLogs {
return fmt.Errorf(`Failed to umount storage volume "%s/%s": %w`, sourcePool, sourceVolumeName, err)
}
logger.Warn("Unable to unmount logs storage, daemon restart required")
}
return nil
}
// Rename the existing storage.
if isLogs && os.Getenv("INCUS_DIR") == "" {
sourcePath = "/var/log/incus"
} else if util.PathExists(internalUtil.VarPath(storageType)) {
sourcePath = internalUtil.VarPath(storageType) + ".temp"
err = os.Rename(internalUtil.VarPath(storageType), sourcePath)
if err != nil {
return fmt.Errorf("Failed to rename existing storage %q: %w", internalUtil.VarPath(storageType), err)
}
}
// Create the new symlink.
err = os.Symlink(destPath, internalUtil.VarPath(storageType))
if err != nil {
return fmt.Errorf("Failed to create the new symlink at %q: %w", internalUtil.VarPath(storageType), err)
}
// Move the data across.
if isLogs {
err = moveInstanceDirs(sourcePath, destPath)
if err != nil {
return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
}
} else {
err = moveContent(sourcePath, destPath)
if err != nil {
return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
}
}
// Remove the old data.
if sourcePath != "/var/log/incus" {
err = os.RemoveAll(sourcePath)
if err != nil {
return fmt.Errorf("Failed to cleanup old directory %q: %w", sourcePath, err)
}
}
return nil
}