The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/6466
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) ===
From 52bbc8b5e62784d1f34f7fa28da374d5d66f45c5 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <thomas.h...@canonical.com> Date: Thu, 14 Nov 2019 10:59:51 +0100 Subject: [PATCH 1/4] lxd/storage: Move storage_cgo.go to drivers package Signed-off-by: Thomas Hipp <thomas.h...@canonical.com> --- lxd/storage/{storage_cgo.go => drivers/driver_cgo.go} | 6 +++--- lxd/storage_btrfs.go | 2 +- lxd/storage_lvm.go | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) rename lxd/storage/{storage_cgo.go => drivers/driver_cgo.go} (98%) diff --git a/lxd/storage/storage_cgo.go b/lxd/storage/drivers/driver_cgo.go similarity index 98% rename from lxd/storage/storage_cgo.go rename to lxd/storage/drivers/driver_cgo.go index 879e94ff54..fd066095e1 100644 --- a/lxd/storage/storage_cgo.go +++ b/lxd/storage/drivers/driver_cgo.go @@ -1,7 +1,7 @@ // +build linux // +build cgo -package storage +package drivers import ( "fmt" @@ -29,8 +29,8 @@ import ( #include <sys/stat.h> #include <sys/types.h> -#include "../include/macro.h" -#include "../include/memory_utils.h" +#include "../../include/macro.h" +#include "../../include/memory_utils.h" #define LXD_MAXPATH 4096 #define LXD_NUMSTRLEN64 21 diff --git a/lxd/storage_btrfs.go b/lxd/storage_btrfs.go index 1d31793083..335ebd5a51 100644 --- a/lxd/storage_btrfs.go +++ b/lxd/storage_btrfs.go @@ -426,7 +426,7 @@ func (s *storageBtrfs) StoragePoolMount() (bool, error) { // Since we mount the loop device LO_FLAGS_AUTOCLEAR is // fine since the loop device will be kept around for as // long as the mount exists. - loopF, loopErr := driver.PrepareLoopDev(source, driver.LoFlagsAutoclear) + loopF, loopErr := drivers.PrepareLoopDev(source, drivers.LoFlagsAutoclear) if loopErr != nil { return false, loopErr } diff --git a/lxd/storage_lvm.go b/lxd/storage_lvm.go index ce7aa01137..62b7421946 100644 --- a/lxd/storage_lvm.go +++ b/lxd/storage_lvm.go @@ -18,6 +18,7 @@ import ( "github.com/lxc/lxd/lxd/project" "github.com/lxc/lxd/lxd/rsync" driver "github.com/lxc/lxd/lxd/storage" + "github.com/lxc/lxd/lxd/storage/drivers" "github.com/lxc/lxd/shared" "github.com/lxc/lxd/shared/api" "github.com/lxc/lxd/shared/ioprogress" @@ -395,7 +396,7 @@ func (s *storageLvm) StoragePoolDelete() error { if s.loopInfo != nil { // Set LO_FLAGS_AUTOCLEAR before we remove the loop file // otherwise we will get EBADF. - err = driver.SetAutoclearOnLoopDev(int(s.loopInfo.Fd())) + err = drivers.SetAutoclearOnLoopDev(int(s.loopInfo.Fd())) if err != nil { logger.Warnf("Failed to set LO_FLAGS_AUTOCLEAR on loop device: %s, manual cleanup needed", err) } @@ -467,12 +468,12 @@ func (s *storageLvm) StoragePoolMount() (bool, error) { if filepath.IsAbs(source) && !shared.IsBlockdevPath(source) { // Try to prepare new loop device. - loopF, loopErr := driver.PrepareLoopDev(source, 0) + loopF, loopErr := drivers.PrepareLoopDev(source, 0) if loopErr != nil { return false, loopErr } // Make sure that LO_FLAGS_AUTOCLEAR is unset. - loopErr = driver.UnsetAutoclearOnLoopDev(int(loopF.Fd())) + loopErr = drivers.UnsetAutoclearOnLoopDev(int(loopF.Fd())) if loopErr != nil { return false, loopErr } From 5d4ced5218e338af759102ba4c8033d8184eec6a Mon Sep 17 00:00:00 2001 From: Thomas Hipp <thomas.h...@canonical.com> Date: Mon, 18 Nov 2019 15:18:18 +0100 Subject: [PATCH 2/4] lxd/storage/drivers: Move FS and mount functions This moves FS and mount functions to the drivers package. Signed-off-by: Thomas Hipp <thomas.h...@canonical.com> --- lxd/storage/drivers/utils.go | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/lxd/storage/drivers/utils.go b/lxd/storage/drivers/utils.go index 71a496c72a..f47b8191eb 100644 --- a/lxd/storage/drivers/utils.go +++ b/lxd/storage/drivers/utils.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "time" "golang.org/x/sys/unix" @@ -14,6 +15,11 @@ import ( "github.com/lxc/lxd/shared/api" ) +// MkfsOptions represents options for filesystem creation. +type MkfsOptions struct { + Label string +} + func wipeDirectory(path string) error { // List all entries entries, err := ioutil.ReadDir(path) @@ -235,3 +241,93 @@ func createSparseFile(filePath string, sizeBytes int64) error { return nil } + +// MakeFSType creates the provided filesystem. +func MakeFSType(path string, fsType string, options *MkfsOptions) (string, error) { + var err error + var msg string + + fsOptions := options + if fsOptions == nil { + fsOptions = &MkfsOptions{} + } + + cmd := []string{fmt.Sprintf("mkfs.%s", fsType), path} + if fsOptions.Label != "" { + cmd = append(cmd, "-L", fsOptions.Label) + } + + if fsType == "ext4" { + cmd = append(cmd, "-E", "nodiscard,lazy_itable_init=0,lazy_journal_init=0") + } + + msg, err = shared.TryRunCommand(cmd[0], cmd[1:]...) + if err != nil { + return msg, err + } + + return "", nil +} + +// Export the mount options map since we might find it useful in other parts of +// LXD. +type mountOptions struct { + capture bool + flag uintptr +} + +// MountOptions represents a list of possible mount options. +var MountOptions = map[string]mountOptions{ + "async": {false, unix.MS_SYNCHRONOUS}, + "atime": {false, unix.MS_NOATIME}, + "bind": {true, unix.MS_BIND}, + "defaults": {true, 0}, + "dev": {false, unix.MS_NODEV}, + "diratime": {false, unix.MS_NODIRATIME}, + "dirsync": {true, unix.MS_DIRSYNC}, + "exec": {false, unix.MS_NOEXEC}, + "lazytime": {true, unix.MS_LAZYTIME}, + "mand": {true, unix.MS_MANDLOCK}, + "noatime": {true, unix.MS_NOATIME}, + "nodev": {true, unix.MS_NODEV}, + "nodiratime": {true, unix.MS_NODIRATIME}, + "noexec": {true, unix.MS_NOEXEC}, + "nomand": {false, unix.MS_MANDLOCK}, + "norelatime": {false, unix.MS_RELATIME}, + "nostrictatime": {false, unix.MS_STRICTATIME}, + "nosuid": {true, unix.MS_NOSUID}, + "rbind": {true, unix.MS_BIND | unix.MS_REC}, + "relatime": {true, unix.MS_RELATIME}, + "remount": {true, unix.MS_REMOUNT}, + "ro": {true, unix.MS_RDONLY}, + "rw": {false, unix.MS_RDONLY}, + "strictatime": {true, unix.MS_STRICTATIME}, + "suid": {false, unix.MS_NOSUID}, + "sync": {true, unix.MS_SYNCHRONOUS}, +} + +// resolveMountOptions resolves the provided mount options. +func resolveMountOptions(options string) (uintptr, string) { + mountFlags := uintptr(0) + tmp := strings.SplitN(options, ",", -1) + for i := 0; i < len(tmp); i++ { + opt := tmp[i] + do, ok := MountOptions[opt] + if !ok { + continue + } + + if do.capture { + mountFlags |= do.flag + } else { + mountFlags &= ^do.flag + } + + copy(tmp[i:], tmp[i+1:]) + tmp[len(tmp)-1] = "" + tmp = tmp[:len(tmp)-1] + i-- + } + + return mountFlags, strings.Join(tmp, ",") +} From dbeef7e28356b9121a8a915d24447b09a32c850b Mon Sep 17 00:00:00 2001 From: Thomas Hipp <thomas.h...@canonical.com> Date: Mon, 18 Nov 2019 15:20:30 +0100 Subject: [PATCH 3/4] lxd/storage: Mark fs/mount functions as deprecated This marks some FS and mount functions as deprecated as they have been moved to the drivers package. Signed-off-by: Thomas Hipp <thomas.h...@canonical.com> --- lxd/storage/utils.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lxd/storage/utils.go b/lxd/storage/utils.go index 25e8e273fd..faa77878a6 100644 --- a/lxd/storage/utils.go +++ b/lxd/storage/utils.go @@ -31,6 +31,7 @@ var baseDirectories = map[drivers.VolumeType][]string{ var VolumeUsedByInstancesWithProfiles func(s *state.State, poolName string, volumeName string, volumeTypeName string, runningOnly bool) ([]string, error) // MkfsOptions represents options for filesystem creation. +// Deprecated: Use drivers.MkfsOptions. type MkfsOptions struct { Label string } @@ -73,6 +74,7 @@ var MountOptions = map[string]mountOptions{ } // LXDResolveMountoptions resolves the provided mount options. +// Deprecated: Use drivers.LXDResolveMountoptions. func LXDResolveMountoptions(options string) (uintptr, string) { mountFlags := uintptr(0) tmp := strings.SplitN(options, ",", -1) @@ -225,6 +227,7 @@ func LXDUsesPool(dbObj *db.Cluster, onDiskPoolName string, driver string, onDisk } // MakeFSType creates the provided filesystem. +// Deprecated: Use drivers.MakeFSType. func MakeFSType(path string, fsType string, options *MkfsOptions) (string, error) { var err error var msg string From 89052290142543986d19eeba71269c829b1aa30b Mon Sep 17 00:00:00 2001 From: Thomas Hipp <thomas.h...@canonical.com> Date: Mon, 18 Nov 2019 15:24:41 +0100 Subject: [PATCH 4/4] lxd/storage/drivers: Add btrfs This adds the btrfs storage driver. Signed-off-by: Thomas Hipp <thomas.h...@canonical.com> --- lxd/storage/drivers/driver_btrfs.go | 740 ++++++++++++++++++++++ lxd/storage/drivers/driver_btrfs_utils.go | 380 +++++++++++ lxd/storage/drivers/load.go | 1 + 3 files changed, 1121 insertions(+) create mode 100644 lxd/storage/drivers/driver_btrfs.go create mode 100644 lxd/storage/drivers/driver_btrfs_utils.go diff --git a/lxd/storage/drivers/driver_btrfs.go b/lxd/storage/drivers/driver_btrfs.go new file mode 100644 index 0000000000..c26d7d75a9 --- /dev/null +++ b/lxd/storage/drivers/driver_btrfs.go @@ -0,0 +1,740 @@ +package drivers + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + + "golang.org/x/sys/unix" + + "github.com/lxc/lxd/lxd/migration" + "github.com/lxc/lxd/lxd/operations" + "github.com/lxc/lxd/lxd/storage/quota" + "github.com/lxc/lxd/shared" + "github.com/lxc/lxd/shared/api" + log "github.com/lxc/lxd/shared/log15" + "github.com/lxc/lxd/shared/units" +) + +var btrfsVersion string +var btrfsLoaded bool + +type btrfs struct { + common + + remount uintptr +} + +func (d *btrfs) load() error { + if btrfsLoaded { + return nil + } + + // Validate the required binaries. + for _, tool := range []string{"btrfs"} { + _, err := exec.LookPath(tool) + if err != nil { + return fmt.Errorf("Required tool '%s' is missing", tool) + } + } + + // Detect and record the version. + if btrfsVersion == "" { + out, err := shared.RunCommand("btrfs", "version") + if err != nil { + return err + } + + count, err := fmt.Sscanf(strings.SplitN(out, " ", 2)[1], "v%s\n", &btrfsVersion) + if err != nil || count != 1 { + return fmt.Errorf("The 'btrfs' tool isn't working properly") + } + } + + btrfsLoaded = true + return nil +} + +// Info returns info about the driver and its environment. +func (d *btrfs) Info() Info { + return Info{ + Name: "btrfs", + Version: btrfsVersion, + OptimizedImages: false, + PreservesInodes: !d.state.OS.RunningInUserNS, + Remote: false, + VolumeTypes: []VolumeType{VolumeTypeCustom, VolumeTypeImage, VolumeTypeContainer, VolumeTypeVM}, + BlockBacking: false, + RunningQuotaResize: true, + } +} + +func (d *btrfs) Create() error { + // WARNING: The Create() function cannot rely on any of the struct attributes being set. + + // Set default source if missing. + defaultSource := filepath.Join(shared.VarPath("disks"), fmt.Sprintf("%s.img", d.name)) + source := d.config["source"] + + if source == "" { + source = defaultSource + d.config["source"] = source + } else if strings.HasPrefix(source, "/") { + source = shared.HostPath(source) + } else { + return fmt.Errorf("Invalid \"source\" property") + } + + poolMntPoint := GetPoolMountPath(d.name) + isBlockDev := false + + if source == defaultSource { + size, err := units.ParseByteSizeString(d.config["size"]) + if err != nil { + return err + } + + err = createSparseFile(source, size) + if err != nil { + return fmt.Errorf("Failed to create sparse file %q: %s", source, err) + } + + output, err := MakeFSType(source, "btrfs", &MkfsOptions{Label: d.name}) + if err != nil { + return fmt.Errorf("Failed to create btrfs: %v (%s)", err, output) + } + } else { + isBlockDev = shared.IsBlockdevPath(source) + + if isBlockDev { + output, err := MakeFSType(source, "btrfs", &MkfsOptions{Label: d.name}) + if err != nil { + return fmt.Errorf("Failed to create btrfs: %v (%s)", err, output) + } + } else { + if isBtrfsSubvolume(source) { + subvols, err := btrfsSubvolumesGet(source) + if err != nil { + return fmt.Errorf("Could not determine if existing btrfs subvolume ist empty: %s", err) + } + if len(subvols) > 0 { + return fmt.Errorf("Requested btrfs subvolume exists but is not empty") + } + } else { + cleanSource := filepath.Clean(source) + lxdDir := shared.VarPath() + + if shared.PathExists(source) && !isOnBtrfs(source) { + return fmt.Errorf("Existing path is neither a btrfs subvolume nor does it reside on a btrfs filesystem") + } else if strings.HasPrefix(cleanSource, lxdDir) { + if cleanSource != poolMntPoint { + return fmt.Errorf("btrfs subvolumes requests in LXD directory %q are only valid under %q\n(e.g. source=%s)", shared.VarPath(), shared.VarPath("storage-pools"), poolMntPoint) + } else if d.state.OS.BackingFS != "btrfs" { + return fmt.Errorf("Creation of btrfs subvolume requested but %q does not reside on btrfs filesystem", source) + } + } + + err := btrfsSubvolumeCreate(source) + if err != nil { + return err + } + } + } + } + + var err error + var devUUID string + mountFlags, mountOptions := resolveMountOptions(d.getMountOptions()) + mountFlags |= d.remount + + if isBlockDev { + devUUID, _ = shared.LookupUUIDByBlockDevPath(source) + // The symlink might not have been created even with the delay + // we granted it above. So try to call btrfs filesystem show and + // parse it out. (I __hate__ this!) + if devUUID == "" { + devUUID, err = btrfsLookupFsUUID(source) + if err != nil { + return err + } + } + d.config["source"] = devUUID + + // If the symlink in /dev/disk/by-uuid hasn't been created yet + // aka we only detected it by parsing btrfs filesystem show, we + // cannot call StoragePoolMount() since it will try to do the + // reverse operation. So instead we shamelessly mount using the + // block device path at the time of pool creation. + err = tryMount(source, GetPoolMountPath(d.name), "btrfs", mountFlags, mountOptions) + } else { + _, err = d.Mount() + } + if err != nil { + return err + } + + // Create default subvolumes. + subvolumes := []string{ + filepath.Join(poolMntPoint, "containers"), + filepath.Join(poolMntPoint, "containers-snapshots"), + filepath.Join(poolMntPoint, "custom"), + filepath.Join(poolMntPoint, "custom-snapshots"), + filepath.Join(poolMntPoint, "images"), + filepath.Join(poolMntPoint, "virtual-machines"), + filepath.Join(poolMntPoint, "virtual-machines-snapshots"), + } + + for _, subvol := range subvolumes { + err := btrfsSubvolumeCreate(subvol) + if err != nil { + return fmt.Errorf("Could not create btrfs subvolume: %s", subvol) + } + } + + return nil +} + +// Delete removes the storage pool from the storage device. +func (d *btrfs) Delete(op *operations.Operation) error { + source := d.config["source"] + + if source == "" { + return fmt.Errorf("no \"source\" property found for the storage pool") + } + + if strings.HasPrefix(source, "/") { + source = shared.HostPath(d.config["source"]) + } + + poolMntPoint := GetPoolMountPath(d.name) + + // Delete default subvolumes. + subvolumes := []string{ + filepath.Join(poolMntPoint, "containers"), + filepath.Join(poolMntPoint, "containers-snapshots"), + filepath.Join(poolMntPoint, "custom"), + filepath.Join(poolMntPoint, "custom-snapshots"), + filepath.Join(poolMntPoint, "images"), + filepath.Join(poolMntPoint, "virtual-machines"), + filepath.Join(poolMntPoint, "virtual-machines-snapshots"), + } + + for _, subvol := range subvolumes { + err := btrfsSubvolumesDelete(subvol) + if err != nil { + return fmt.Errorf("Could not delete btrfs subvolume: %s", subvol) + } + } + + _, err := d.Unmount() + if err != nil { + return err + } + + if filepath.IsAbs(source) { + var err error + cleanSource := filepath.Clean(source) + sourcePath := shared.VarPath("disks", d.name) + loopFilePath := sourcePath + ".img" + + if cleanSource == loopFilePath { + // This is a loop file so simply remove it. + err = os.Remove(source) + } else { + if !isBtrfsFilesystem(source) && isBtrfsSubvolume(source) { + err = btrfsSubvolumesDelete(source) + } + } + if err != nil && !os.IsNotExist(err) { + return err + } + } + + return wipeDirectory(poolMntPoint) +} + +// Mount mounts the storage pool. +func (d *btrfs) Mount() (bool, error) { + source := d.config["source"] + + if source == "" { + return false, fmt.Errorf("no \"source\" property found for the storage pool") + } + + if strings.HasPrefix(source, "/") { + source = shared.HostPath(d.config["source"]) + } + + poolMntPoint := GetPoolMountPath(d.name) + + // Check whether the mount poolMntPoint exits. + if !shared.PathExists(poolMntPoint) { + err := os.MkdirAll(poolMntPoint, 0711) + if err != nil { + return false, err + } + } + + if shared.IsMountPoint(poolMntPoint) && (d.remount&unix.MS_REMOUNT) == 0 { + return false, nil + } + + mountFlags, mountOptions := resolveMountOptions(d.getMountOptions()) + mountSource := source + isBlockDev := shared.IsBlockdevPath(source) + + if filepath.IsAbs(source) { + cleanSource := filepath.Clean(source) + loopFilePath := shared.VarPath("disks", d.name+".img") + + if !isBlockDev && cleanSource == loopFilePath { + // If source == "${LXD_DIR}"/disks/{pool_name} it is a + // loop file we're dealing with. + // + // Since we mount the loop device LO_FLAGS_AUTOCLEAR is + // fine since the loop device will be kept around for as + // long as the mount exists. + loopF, loopErr := PrepareLoopDev(source, LoFlagsAutoclear) + if loopErr != nil { + return false, loopErr + } + mountSource = loopF.Name() + defer loopF.Close() + } else if !isBlockDev && cleanSource != poolMntPoint { + mountSource = source + mountFlags |= unix.MS_BIND + } else if !isBlockDev && cleanSource == poolMntPoint && d.state.OS.BackingFS == "btrfs" { + return false, nil + } + // User is using block device path. + } else { + // Try to lookup the disk device by UUID but don't fail. If we + // don't find one this might just mean we have been given the + // UUID of a subvolume. + byUUID := fmt.Sprintf("/dev/disk/by-uuid/%s", source) + diskPath, err := os.Readlink(byUUID) + if err == nil { + mountSource = fmt.Sprintf("/dev/%s", strings.Trim(diskPath, "../../")) + } else { + // We have very likely been given a subvolume UUID. In + // this case we should simply assume that the user has + // mounted the parent of the subvolume or the subvolume + // itself. Otherwise this becomes a really messy + // detection task. + return false, nil + } + } + + mountFlags |= d.remount + err := unix.Mount(mountSource, poolMntPoint, "btrfs", mountFlags, mountOptions) + if err != nil { + return false, err + } + + return true, nil +} + +// Unmount unmounts the storage pool. +func (d *btrfs) Unmount() (bool, error) { + poolMntPoint := GetPoolMountPath(d.name) + return forceUnmount(poolMntPoint) +} + +func (d *btrfs) GetResources() (*api.ResourcesStoragePool, error) { + // Use the generic VFS resources. + return vfsResources(GetPoolMountPath(d.name)) +} + +// GetVolumeUsage returns the disk space used by the volume. +func (d *btrfs) GetVolumeUsage(volType VolumeType, volName string) (int64, error) { + return btrfsPoolVolumeQGroupUsage(GetVolumeMountPath(d.name, volType, volName)) +} + +// ValidateVolume validates the supplied volume config. +func (d *btrfs) ValidateVolume(vol Volume, removeUnknownKeys bool) error { + return d.validateVolume(vol, nil, removeUnknownKeys) +} + +// HasVolume indicates whether a specific volume exists on the storage pool. +func (d *btrfs) HasVolume(volType VolumeType, volName string) bool { + return isBtrfsSubvolume(GetVolumeMountPath(d.name, volType, volName)) +} + +// GetVolumeDiskPath returns the location and file format of a disk volume. +func (d *btrfs) GetVolumeDiskPath(volType VolumeType, volName string) (string, error) { + return filepath.Join(GetVolumeMountPath(d.name, volType, volName), "root.img"), nil +} + +// CreateVolume creates an empty volume and can optionally fill it by executing the supplied +// filler function. +func (d *btrfs) CreateVolume(vol Volume, filler func(mountPath, rootBlockPath string) error, op *operations.Operation) error { + volPath := vol.MountPath() + + err := btrfsSubvolumeCreate(volPath) + if err != nil { + return err + } + + revertSubvolume := true + defer func() { + if revertSubvolume { + btrfsSubvolumeDelete(volPath) + } + }() + + // Extract specified size from pool or volume config. + size := d.config["volume.size"] + if vol.config["size"] != "" { + size = vol.config["size"] + } + + // Create sparse loopback file if volume is block. + rootBlockPath := "" + if vol.contentType == ContentTypeBlock { + // We expect the filler to copy the VM image into this path. + rootBlockPath, err = d.GetVolumeDiskPath(vol.volType, vol.name) + if err != nil { + return err + } + } else { + // Get the volume ID for the new volume, which is used to set project quota. + volID, err := d.getVolID(vol.volType, vol.name) + if err != nil { + return err + } + + // Initialise the volume's quota using the volume ID. + err = d.initQuota(volPath, volID) + if err != nil { + return err + } + + defer func() { + if revertSubvolume { + d.deleteQuota(volPath, volID) + } + }() + + // Set the quota. + err = d.setQuota(volPath, volID, size) + if err != nil { + return err + } + } + + // Run the volume filler function if supplied. + if filler != nil { + err = filler(volPath, rootBlockPath) + if err != nil { + return err + } + } + + // If we are creating a block volume, resize it to the requested size or 10GB. + // We expect the filler function to have converted the qcow2 image to raw into the rootBlockPath. + if vol.contentType == ContentTypeBlock { + blockSize := size + if blockSize == "" { + blockSize = "10GB" + } + + blockSizeBytes, err := units.ParseByteSizeString(blockSize) + if err != nil { + return err + } + + if shared.PathExists(rootBlockPath) { + _, err = shared.RunCommand("qemu-img", "resize", "-f", "raw", rootBlockPath, fmt.Sprintf("%d", blockSizeBytes)) + if err != nil { + return fmt.Errorf("Failed resizing disk image %s to size %s: %v", rootBlockPath, blockSize, err) + } + } else { + // If rootBlockPath doesn't exist, then there has been no filler function + // supplied to create it from another source. So instead create an empty + // volume (use for PXE booting a VM). + _, err = shared.RunCommand("qemu-img", "create", "-f", "raw", rootBlockPath, fmt.Sprintf("%d", blockSizeBytes)) + if err != nil { + return fmt.Errorf("Failed creating disk image %s as size %s: %v", rootBlockPath, blockSize, err) + } + } + } + + revertSubvolume = false + return nil +} + +// MigrateVolume sends a volume for migration. +func (d *btrfs) MigrateVolume(vol Volume, conn io.ReadWriteCloser, volSrcArgs migration.VolumeSourceArgs, op *operations.Operation) error { + return ErrNotImplemented +} + +// CreateVolumeFromMigration creates a volume being sent via a migration. +func (d *btrfs) CreateVolumeFromMigration(vol Volume, conn io.ReadWriteCloser, volTargetArgs migration.VolumeTargetArgs, op *operations.Operation) error { + return ErrNotImplemented +} + +// CreateVolumeFromCopy provides same-pool volume copying functionality. +func (d *btrfs) CreateVolumeFromCopy(vol Volume, srcVol Volume, copySnapshots bool, op *operations.Operation) error { + err := btrfsSubvolumesSnapshotCreate(srcVol.MountPath(), vol.MountPath(), false, true, d.state.OS.RunningInUserNS) + if err != nil { + return err + } + + if !copySnapshots || srcVol.IsSnapshot() { + return nil + } + + snapshots, err := d.VolumeSnapshots(srcVol.volType, srcVol.name, op) + if err != nil { + return err + } + + if len(snapshots) == 0 { + return nil + } + + for _, snap := range snapshots { + srcSnapshot := GetVolumeMountPath(d.name, srcVol.volType, GetSnapshotVolumeName(srcVol.name, snap)) + dstSnapshot := GetVolumeMountPath(d.name, vol.volType, GetSnapshotVolumeName(vol.name, snap)) + + err = btrfsSubvolumeSnapshotCreate(srcSnapshot, dstSnapshot, true, d.state.OS.RunningInUserNS) + if err != nil { + return err + } + } + + return nil +} + +// VolumeSnapshots returns a list of snapshots for the volume. +func (d *btrfs) VolumeSnapshots(volType VolumeType, volName string, op *operations.Operation) ([]string, error) { + return btrfsSubvolumeSnapshotsGet(GetVolumeSnapshotDir(d.name, volType, volName)) +} + +// UpdateVolume applies config changes to the volume. +func (d *btrfs) UpdateVolume(vol Volume, changedConfig map[string]string) error { + if vol.contentType != ContentTypeFS { + return fmt.Errorf("Content type not supported") + } + + if vol.volType != VolumeTypeCustom { + return fmt.Errorf("Volume type not supported") + } + + return d.SetVolumeQuota(vol.volType, vol.name, vol.config["size"], nil) +} + +// RenameVolume renames a volume and its snapshots. +func (d *btrfs) RenameVolume(volType VolumeType, volName string, newVolName string, op *operations.Operation) error { + srcVolumePath := GetVolumeMountPath(d.name, volType, volName) + dstVolumePath := GetVolumeMountPath(d.name, volType, newVolName) + + err := os.Rename(srcVolumePath, dstVolumePath) + if err != nil { + return err + } + + srcSnapshotDir := GetVolumeSnapshotDir(d.name, volType, volName) + dstSnapshotDir := GetVolumeSnapshotDir(d.name, volType, newVolName) + + err = os.Rename(srcSnapshotDir, dstSnapshotDir) + if err != nil { + return err + } + + return nil +} + +// RestoreVolume restores a volume from a snapshot. +func (d *btrfs) RestoreVolume(vol Volume, snapshotName string, op *operations.Operation) error { + source := GetVolumeMountPath(d.name, vol.volType, GetSnapshotVolumeName(vol.name, snapshotName)) + + return btrfsSubvolumesSnapshotCreate(source, vol.MountPath(), false, true, d.state.OS.RunningInUserNS) +} + +// DeleteVolume deletes a volume of the storage device. If any snapshots of the volume remain then +// this function will return an error. +func (d *btrfs) DeleteVolume(volType VolumeType, volName string, op *operations.Operation) error { + snapshots, err := d.VolumeSnapshots(volType, volName, op) + if err != nil { + return err + } + + if len(snapshots) > 0 { + return fmt.Errorf("Cannot remove a volume that has snapshots") + } + + volPath := GetVolumeMountPath(d.name, volType, volName) + + if !shared.PathExists(volPath) || !isBtrfsSubvolume(volPath) { + return nil + } + + err = btrfsSubvolumeDelete(volPath) + if err != nil { + return err + } + + // Although the volume snapshot directory should already be removed, lets remove it here + // to just in case the top-level directory is left. + err = deleteParentSnapshotDirIfEmpty(d.name, volType, volName) + if err != nil { + return err + } + + return nil +} + +// MountVolume simulates mounting a volume. As dir driver doesn't have volumes to mount it returns +// false indicating that there is no need to issue an unmount. +func (d *btrfs) MountVolume(volType VolumeType, volName string, op *operations.Operation) (bool, error) { + return false, nil +} + +// MountVolumeSnapshot sets up a read-only mount on top of the snapshot to avoid accidental modifications. +func (d *btrfs) MountVolumeSnapshot(volType VolumeType, volName, snapshotName string, op *operations.Operation) (bool, error) { + return false, ErrNotImplemented +} + +// UnmountVolume simulates unmounting a volume. As dir driver doesn't have volumes to unmount it +// returns false indicating the volume was already unmounted. +func (d *btrfs) UnmountVolume(volType VolumeType, volName string, op *operations.Operation) (bool, error) { + return false, nil +} + +// UnmountVolumeSnapshot removes the read-only mount placed on top of a snapshot. +func (d *btrfs) UnmountVolumeSnapshot(volType VolumeType, volName, snapshotName string, op *operations.Operation) (bool, error) { + return false, nil +} + +// SetVolumeQuota sets the quota on the volume. +func (d *btrfs) SetVolumeQuota(volType VolumeType, volName, size string, op *operations.Operation) error { + volPath := GetVolumeMountPath(d.name, volType, volName) + + volID, err := d.getVolID(volType, volName) + if err != nil { + return err + } + + return d.setQuota(volPath, volID, size) +} + +// quotaProjectID generates a project quota ID from a volume ID. +func (d *btrfs) quotaProjectID(volID int64) uint32 { + return uint32(volID + 10000) +} + +// initQuota initialises the project quota on the path. The volID generates a quota project ID. +func (d *btrfs) initQuota(path string, volID int64) error { + if volID == 0 { + return fmt.Errorf("Missing volume ID") + } + + ok, err := quota.Supported(path) + if err != nil || !ok { + // Skipping quota as underlying filesystem doesn't suppport project quotas. + return nil + } + + err = quota.SetProject(path, d.quotaProjectID(volID)) + if err != nil { + return err + } + + return nil +} + +// setQuota sets the project quota on the path. The volID generates a quota project ID. +func (d *btrfs) setQuota(path string, volID int64, size string) error { + if volID == 0 { + return fmt.Errorf("Missing volume ID") + } + + // If size not specified in volume config, then use pool's default volume.size setting. + if size == "" || size == "0" { + size = d.config["volume.size"] + } + + sizeBytes, err := units.ParseByteSizeString(size) + if err != nil { + return err + } + + ok, err := quota.Supported(path) + if err != nil || !ok { + if sizeBytes > 0 { + // Skipping quota as underlying filesystem doesn't suppport project quotas. + d.logger.Warn("The backing filesystem doesn't support quotas, skipping quota", log.Ctx{"path": path}) + } + return nil + } + + err = quota.SetProjectQuota(path, d.quotaProjectID(volID), sizeBytes) + if err != nil { + return err + } + + return nil +} + +// deleteQuota removes the project quota for a volID from a path. +func (d *btrfs) deleteQuota(path string, volID int64) error { + if volID == 0 { + return fmt.Errorf("Missing volume ID") + } + + ok, err := quota.Supported(path) + if err != nil || !ok { + // Skipping quota as underlying filesystem doesn't suppport project quotas. + return nil + } + + err = quota.SetProject(path, 0) + if err != nil { + return err + } + + err = quota.SetProjectQuota(path, d.quotaProjectID(volID), 0) + if err != nil { + return err + } + + return nil +} + +// CreateVolumeSnapshot creates a snapshot of a volume. +func (d *btrfs) CreateVolumeSnapshot(volType VolumeType, volName string, newSnapshotName string, op *operations.Operation) error { + sourcePath := GetVolumeMountPath(d.name, volType, volName) + targetPath := GetVolumeMountPath(d.name, volType, newSnapshotName) + + return btrfsSubvolumesSnapshotCreate(sourcePath, targetPath, true, true, d.state.OS.RunningInUserNS) +} + +// DeleteVolumeSnapshot removes a snapshot from the storage device. The volName and snapshotName +// must be bare names and should not be in the format "volume/snapshot". +func (d *btrfs) DeleteVolumeSnapshot(volType VolumeType, volName string, snapshotName string, op *operations.Operation) error { + fullSnapshotName := GetSnapshotVolumeName(volName, snapshotName) + return btrfsSubvolumesDelete(GetVolumeMountPath(d.name, volType, fullSnapshotName)) +} + +// RenameVolumeSnapshot renames a volume snapshot. +func (d *btrfs) RenameVolumeSnapshot(volType VolumeType, volName string, snapshotName string, newSnapshotName string, op *operations.Operation) error { + oldFullSnapshotName := GetSnapshotVolumeName(volName, snapshotName) + newFullSnapshotName := GetSnapshotVolumeName(volName, newSnapshotName) + + oldPath := GetVolumeMountPath(d.name, volType, oldFullSnapshotName) + newPath := GetVolumeMountPath(d.name, volType, newFullSnapshotName) + + return os.Rename(oldPath, newPath) +} + +func (d *btrfs) getMountOptions() string { + if d.config["btrfs.mount_options"] != "" { + return d.config["btrfs.mount_options"] + } + + return "user_subvol_rm_allowed" +} diff --git a/lxd/storage/drivers/driver_btrfs_utils.go b/lxd/storage/drivers/driver_btrfs_utils.go new file mode 100644 index 0000000000..47e5ec2b93 --- /dev/null +++ b/lxd/storage/drivers/driver_btrfs_utils.go @@ -0,0 +1,380 @@ +package drivers + +import ( + "fmt" + "os" + "path" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/lxc/lxd/lxd/util" + "github.com/lxc/lxd/shared" + "github.com/lxc/lxd/shared/logger" + "golang.org/x/sys/unix" +) + +var errBtrfsNoQuota = fmt.Errorf("Quotas disabled on filesystem") +var errBtrfsNoQGroup = fmt.Errorf("Unable to find quota group") + +func btrfsSubvolumeCreate(subvol string) error { + parentDestPath := filepath.Dir(subvol) + if !shared.PathExists(parentDestPath) { + err := os.MkdirAll(parentDestPath, 0711) + if err != nil { + return err + } + } + + _, err := shared.RunCommand( + "btrfs", + "subvolume", + "create", + subvol) + if err != nil { + logger.Errorf("Failed to create BTRFS subvolume \"%s\": %v", subvol, err) + return err + } + + return nil +} + +func btrfsSubvolumesGet(path string) ([]string, error) { + result := []string{} + + if !strings.HasSuffix(path, "/") { + path = path + "/" + } + + // Unprivileged users can't get to fs internals + filepath.Walk(path, func(fpath string, fi os.FileInfo, err error) error { + // Skip walk errors + if err != nil { + return nil + } + + // Ignore the base path + if strings.TrimRight(fpath, "/") == strings.TrimRight(path, "/") { + return nil + } + + // Subvolumes can only be directories + if !fi.IsDir() { + return nil + } + + // Check if a btrfs subvolume + if isBtrfsSubvolume(fpath) { + result = append(result, strings.TrimPrefix(fpath, path)) + } + + return nil + }) + + return result, nil +} + +func btrfsSubvolumeSnapshotsGet(path string) ([]string, error) { + result := []string{} + + if !strings.HasSuffix(path, "/") { + path = path + "/" + } + + // Unprivileged users can't get to fs internals + filepath.Walk(path, func(fpath string, fi os.FileInfo, err error) error { + // Skip walk errors + if err != nil { + return nil + } + + // Ignore the base path + if strings.TrimRight(fpath, "/") == strings.TrimRight(path, "/") { + return nil + } + + // Subvolumes can only be directories + if !fi.IsDir() { + return nil + } + + // Check if a btrfs subvolume snapshot + if isBtrfsSubvolumeSnapshot(fpath) { + result = append(result, strings.TrimPrefix(fpath, path)) + } + + return nil + }) + + return result, nil +} + +// isBtrfsSubvolume returns true if the given Path is a btrfs subvolume else +// false. +func isBtrfsSubvolume(subvolPath string) bool { + fs := unix.Stat_t{} + err := unix.Lstat(subvolPath, &fs) + if err != nil { + return false + } + + // Check if BTRFS_FIRST_FREE_OBJECTID + if fs.Ino != 256 { + return false + } + + return true +} + +// isBtrfsSubvolumeSnapshot returns true if the given Path is a btrfs subvolume +// snapshot else false. +func isBtrfsSubvolumeSnapshot(subvolPath string) bool { + fs := unix.Stat_t{} + err := unix.Lstat(subvolPath, &fs) + if err != nil { + return false + } + + return fs.Ino == 258 +} + +func isOnBtrfs(path string) bool { + fs := unix.Statfs_t{} + + err := unix.Statfs(path, &fs) + if err != nil { + return false + } + + if fs.Type != util.FilesystemSuperMagicBtrfs { + return false + } + + return true +} + +func btrfsLookupFsUUID(fs string) (string, error) { + output, err := shared.RunCommand( + "btrfs", + "filesystem", + "show", + "--raw", + fs) + if err != nil { + return "", fmt.Errorf("failed to detect UUID") + } + + outputString := output + idx := strings.Index(outputString, "uuid: ") + outputString = outputString[idx+6:] + outputString = strings.TrimSpace(outputString) + idx = strings.Index(outputString, "\t") + outputString = outputString[:idx] + outputString = strings.Trim(outputString, "\n") + + return outputString, nil +} + +// btrfsSubvolumesDelete is the recursive variant on btrfsSubvolumeDelete, +// it first deletes subvolumes of the subvolume and then the +// subvolume itself. +func btrfsSubvolumesDelete(subvol string) error { + // Delete subsubvols. + subsubvols, err := btrfsSubvolumesGet(subvol) + if err != nil { + return err + } + sort.Sort(sort.Reverse(sort.StringSlice(subsubvols))) + + for _, subsubvol := range subsubvols { + err := btrfsSubvolumeDelete(path.Join(subvol, subsubvol)) + if err != nil { + return err + } + } + + // Delete the subvol itself + err = btrfsSubvolumeDelete(subvol) + if err != nil { + return err + } + + return nil +} + +func btrfsSubvolumeDelete(subvol string) error { + // Attempt (but don't fail on) to delete any qgroup on the subvolume + qgroup, err := btrfsSubvolumeQGroup(subvol) + if err == nil { + shared.RunCommand( + "btrfs", + "qgroup", + "destroy", + qgroup, + subvol) + } + + // Attempt to make the subvolume writable + shared.RunCommand("btrfs", "property", "set", subvol, "ro", "false") + + // Delete the subvolume itself + _, err = shared.RunCommand( + "btrfs", + "subvolume", + "delete", + subvol) + + return err +} + +func btrfsSubvolumeQGroup(subvol string) (string, error) { + output, err := shared.RunCommand( + "btrfs", + "qgroup", + "show", + "-e", + "-f", + subvol) + + if err != nil { + return "", errBtrfsNoQuota + } + + var qgroup string + for _, line := range strings.Split(output, "\n") { + if line == "" || strings.HasPrefix(line, "qgroupid") || strings.HasPrefix(line, "---") { + continue + } + + fields := strings.Fields(line) + if len(fields) != 4 { + continue + } + + qgroup = fields[0] + } + + if qgroup == "" { + return "", errBtrfsNoQGroup + } + + return qgroup, nil +} + +func isBtrfsFilesystem(path string) bool { + _, err := shared.RunCommand("btrfs", "filesystem", "show", path) + if err != nil { + return false + } + + return true +} + +func btrfsPoolVolumeQGroupUsage(subvol string) (int64, error) { + output, err := shared.RunCommand( + "btrfs", + "qgroup", + "show", + "-e", + "-f", + subvol) + + if err != nil { + return -1, fmt.Errorf("BTRFS quotas not supported. Try enabling them with \"btrfs quota enable\"") + } + + for _, line := range strings.Split(output, "\n") { + if line == "" || strings.HasPrefix(line, "qgroupid") || strings.HasPrefix(line, "---") { + continue + } + + fields := strings.Fields(line) + if len(fields) != 4 { + continue + } + + usage, err := strconv.ParseInt(fields[2], 10, 64) + if err != nil { + continue + } + + return usage, nil + } + + return -1, fmt.Errorf("Unable to find current qgroup usage") +} + +func btrfsSubvolumesSnapshotCreate(source string, dest string, readonly bool, recursive bool, userns bool) error { + // Now snapshot all subvolumes of the root. + if recursive { + // Get a list of subvolumes of the root + subsubvols, err := btrfsSubvolumesGet(source) + if err != nil { + return err + } + sort.Sort(sort.StringSlice(subsubvols)) + + if len(subsubvols) > 0 && readonly { + // A root with subvolumes can never be readonly, + // also don't make subvolumes readonly. + readonly = false + + logger.Warnf("Subvolumes detected, ignoring ro flag") + } + + // First snapshot the root + err = btrfsSubvolumeSnapshotCreate(source, dest, readonly, userns) + if err != nil { + return err + } + + for _, subsubvol := range subsubvols { + // Clear the target for the subvol to use + os.Remove(path.Join(dest, subsubvol)) + + err := btrfsSubvolumeSnapshotCreate(path.Join(source, subsubvol), path.Join(dest, subsubvol), readonly, userns) + if err != nil { + return err + } + } + } else { + err := btrfsSubvolumeSnapshotCreate(source, dest, readonly, userns) + if err != nil { + return err + } + } + + return nil +} + +func btrfsSubvolumeSnapshotCreate(source string, dest string, readonly bool, userns bool) error { + var output string + var err error + if readonly && !userns { + output, err = shared.RunCommand( + "btrfs", + "subvolume", + "snapshot", + "-r", + source, + dest) + } else { + output, err = shared.RunCommand( + "btrfs", + "subvolume", + "snapshot", + source, + dest) + } + if err != nil { + return fmt.Errorf( + "subvolume snapshot failed, source=%s, dest=%s, output=%s", + source, + dest, + output, + ) + } + + return err +} diff --git a/lxd/storage/drivers/load.go b/lxd/storage/drivers/load.go index bced6925e7..34fe37c5d0 100644 --- a/lxd/storage/drivers/load.go +++ b/lxd/storage/drivers/load.go @@ -8,6 +8,7 @@ import ( var drivers = map[string]func() driver{ "dir": func() driver { return &dir{} }, "cephfs": func() driver { return &cephfs{} }, + "btrfs": func() driver { return &btrfs{} }, } // Load returns a Driver for an existing low-level storage pool.
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel