Skip to content

Commit

Permalink
feat(vd): support filesystem mode (#327)
Browse files Browse the repository at this point in the history
Support filesystem mode.
Support qcow2 images.
---------
Signed-off-by: yaroslavborbat <yaroslav.752@gmail.com>
  • Loading branch information
yaroslavborbat authored Sep 5, 2024
1 parent 621baf2 commit 754c4a7
Show file tree
Hide file tree
Showing 22 changed files with 529 additions and 167 deletions.
4 changes: 2 additions & 2 deletions api/core/v1alpha2/vdcondition/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ const (
Provisioning ReadyReason = "Provisioning"
// ProvisioningNotStarted indicates that the provisioning process has not started yet.
ProvisioningNotStarted ReadyReason = "ProvisioningNotStarted"
// WaitForFirstConsumer indicates that the provisioning has been suspended: a created and scheduled virtual machine is awaited.
WaitForFirstConsumer ReadyReason = "WaitForFirstConsumer"
// WaitingForFirstConsumer indicates that the provisioning has been suspended: a created and scheduled virtual machine is awaited.
WaitingForFirstConsumer ReadyReason = "WaitingForFirstConsumer"
// ProvisioningFailed indicates that the provisioning process has failed.
ProvisioningFailed ReadyReason = "ProvisioningFailed"
// Ready indicates that the import process is complete and the `VirtualDisk` is ready for use.
Expand Down
8 changes: 0 additions & 8 deletions api/core/v1alpha2/virtual_disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,10 @@ type VirtualDisk struct {
}

type VirtualDiskSpec struct {
BindingMode *VirtualDiskBindingMode `json:"bindingMode,omitempty"`
DataSource *VirtualDiskDataSource `json:"dataSource,omitempty"`
PersistentVolumeClaim VirtualDiskPersistentVolumeClaim `json:"persistentVolumeClaim"`
}

type VirtualDiskBindingMode string

const (
VirtualDiskBindingModeWaitForFirstConsumer VirtualDiskBindingMode = "WaitForFirstConsumer"
VirtualDiskBindingModeImmediate VirtualDiskBindingMode = "Immediate"
)

type VirtualDiskStatus struct {
DownloadSpeed *StatusSpeed `json:"downloadSpeed,omitempty"`
Capacity string `json:"capacity,omitempty"`
Expand Down
5 changes: 0 additions & 5 deletions api/core/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions crds/doc-ru-virtualdisks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ spec:
properties:
spec:
properties:
bindingMode:
description: |
Типы режимов привязки диска:
* `WaitForFirstConsumer` — отложить создание диска до тех пор, пока использующая этот диск виртуальная машина не будет назначена на узел.
* `Immediate` — начать создание диска, не дожидаясь создания виртуальной машины.
dataSource:
description: |
Тип источника, из которого будет создан диск. Если источник (.spec.dataSource) отсутствует, то будет создан пустой диск.
Expand Down
10 changes: 0 additions & 10 deletions crds/virtualdisks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,6 @@ spec:
spec:
type: object
properties:
bindingMode:
type: string
enum:
- "WaitForFirstConsumer"
- "Immediate"
description: |
The types of disk binding modes are:
* `WaitForFirstConsumer` — delay the provisioning of a disk until a `VirtualMachine` that uses the disk is scheduled.
* `Immediate` — start creating the disk without waiting for the scheduling of the virtual machine.
persistentVolumeClaim:
type: object
description: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
diff --git a/pkg/storagecapabilities/storagecapabilities.go b/pkg/storagecapabilities/storagecapabilities.go
index 24bbac64b..fccdd0c08 100644
--- a/pkg/storagecapabilities/storagecapabilities.go
+++ b/pkg/storagecapabilities/storagecapabilities.go
@@ -8,9 +8,10 @@ import (

v1 "k8s.io/api/core/v1"
storagev1 "k8s.io/api/storage/v1"
- "kubevirt.io/containerized-data-importer/pkg/util"
"sigs.k8s.io/controller-runtime/pkg/client"

+ "kubevirt.io/containerized-data-importer/pkg/util"
+
cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
)

@@ -102,6 +103,9 @@ var CapabilitiesByProvisionerKey = map[string][]StorageCapabilities{
"manila.csi.openstack.org": {{rwx, file}},
// ovirt csi
"csi.ovirt.org": createRWOBlockAndFilesystemCapabilities(),
+ // Deckhouse
+ "replicated.csi.storage.deckhouse.io": createLinstorCapabilities(),
+ "local.csi.storage.deckhouse.io": createTopoLVMCapabilities(),
}

// SourceFormatsByProvisionerKey defines the advised data import cron source format
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
diff --git a/pkg/common/format.go b/pkg/common/format.go
new file mode 100644
index 000000000..06497e582
--- /dev/null
+++ b/pkg/common/format.go
@@ -0,0 +1,22 @@
+package common
+
+import "os"
+
+func GetFormat(path string) (string, error) {
+ const (
+ formatQcow2 = "qcow2"
+ formatRaw = "raw"
+ )
+ info, err := os.Stat(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return formatQcow2, nil
+ }
+ return "", err
+ }
+ mode := info.Mode()
+ if mode&os.ModeDevice != 0 {
+ return formatRaw, nil
+ }
+ return formatQcow2, nil
+}
diff --git a/pkg/image/qemu.go b/pkg/image/qemu.go
index 651fb5fc8..adcfe8824 100644
--- a/pkg/image/qemu.go
+++ b/pkg/image/qemu.go
@@ -61,6 +61,7 @@ type ImgInfo struct {
// QEMUOperations defines the interface for executing qemu subprocesses
type QEMUOperations interface {
ConvertToRawStream(*url.URL, string, bool) error
+ ConvertToFormatStream(url *url.URL, format, dest string, preallocate bool) error
Resize(string, resource.Quantity, bool) error
Info(url *url.URL) (*ImgInfo, error)
Validate(*url.URL, int64) error
@@ -114,6 +115,36 @@ func NewQEMUOperations() QEMUOperations {
return &qemuOperations{}
}

+func convertTo(format, src, dest string, preallocate bool) error {
+ switch format {
+ case "qcow2", "raw":
+ // Do nothing.
+ default:
+ return errors.Errorf("unknown format: %s", format)
+ }
+ args := []string{"convert", "-t", "writeback", "-p", "-O", format, src, dest}
+ var err error
+
+ if preallocate {
+ err = addPreallocation(args, convertPreallocationMethods, func(args []string) ([]byte, error) {
+ return qemuExecFunction(nil, reportProgress, "qemu-img", args...)
+ })
+ } else {
+ klog.V(1).Infof("Running qemu-img with args: %v", args)
+ _, err = qemuExecFunction(nil, reportProgress, "qemu-img", args...)
+ }
+ if err != nil {
+ os.Remove(dest)
+ errorMsg := fmt.Sprintf("could not convert image to %s", format)
+ if nbdkitLog, err := os.ReadFile(common.NbdkitLogPath); err == nil {
+ errorMsg += " " + string(nbdkitLog)
+ }
+ return errors.Wrap(err, errorMsg)
+ }
+
+ return nil
+}
+
func convertToRaw(src, dest string, preallocate bool) error {
args := []string{"convert", "-t", "writeback", "-p", "-O", "raw", src, dest}
var err error
@@ -145,6 +176,13 @@ func (o *qemuOperations) ConvertToRawStream(url *url.URL, dest string, prealloca
return convertToRaw(url.String(), dest, preallocate)
}

+func (o *qemuOperations) ConvertToFormatStream(url *url.URL, format, dest string, preallocate bool) error {
+ if len(url.Scheme) > 0 && url.Scheme != "nbd+unix" {
+ return fmt.Errorf("not valid schema %s", url.Scheme)
+ }
+ return convertTo(format, url.String(), dest, preallocate)
+}
+
// convertQuantityToQemuSize translates a quantity string into a Qemu compatible string.
func convertQuantityToQemuSize(size resource.Quantity) string {
int64Size, asInt := size.AsInt64()
@@ -274,13 +312,17 @@ func CreateBlankImage(dest string, size resource.Quantity, preallocate bool) err

// CreateBlankImage creates a raw image with a given size
func (o *qemuOperations) CreateBlankImage(dest string, size resource.Quantity, preallocate bool) error {
+ format, err := common.GetFormat(dest)
+ if err != nil {
+ return err
+ }
klog.V(3).Infof("image size is %s", size.String())
- args := []string{"create", "-f", "raw", dest, convertQuantityToQemuSize(size)}
+ args := []string{"create", "-f", format, dest, convertQuantityToQemuSize(size)}
if preallocate {
klog.V(1).Infof("Added preallocation")
args = append(args, []string{"-o", "preallocation=falloc"}...)
}
- _, err := qemuExecFunction(nil, nil, "qemu-img", args...)
+ _, err = qemuExecFunction(nil, nil, "qemu-img", args...)
if err != nil {
os.Remove(dest)
return errors.Wrap(err, fmt.Sprintf("could not create raw image with size %s in %s", size.String(), dest))
diff --git a/pkg/importer/data-processor.go b/pkg/importer/data-processor.go
index ca7b2e853..ec24b3b64 100644
--- a/pkg/importer/data-processor.go
+++ b/pkg/importer/data-processor.go
@@ -276,8 +276,13 @@ func (dp *DataProcessor) convert(url *url.URL) (ProcessingPhase, error) {
if err != nil {
return ProcessingPhaseError, err
}
- klog.V(3).Infoln("Converting to Raw")
- err = qemuOperations.ConvertToRawStream(url, dp.dataFile, dp.preallocation)
+ format, err := common.GetFormat(dp.dataFile)
+ if err != nil {
+ return ProcessingPhaseError, errors.Wrap(err, "Unable to get format")
+ }
+
+ klog.V(3).Infoln("Converting to", "format", format)
+ err = qemuOperations.ConvertToFormatStream(url, format, dp.dataFile, dp.preallocation)
if err != nil {
return ProcessingPhaseError, errors.Wrap(err, "Conversion to Raw failed")
}
10 changes: 10 additions & 0 deletions images/cdi-artifact/patches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ Do not manage DataVolume CRD with cdi-operator. Module will install this CRD usi

Set the storage class name for the scratch pvc from the original pvc that will own the scratch pvc, or set it to an empty value if not available.

#### `012-add-caps-for-deckhouse-provisioners.patch`

Add capabilities for deckhouse provisioners to cdi StorageProfile.

#### `013-converting-images-in-filesystem-to-qcow2.patch`

Converting images in the file system to qcow2.

CDI can currently upload virtual machine images to persistent volumes (PVCs). Regardless of the target, whether it's a block device or a file, CDI converts the image to raw format. We're changing this behavior, but only for file targets. Conversion will now happen to the qcow2 format.

#### `014-delete-service-monitor.patch`

Removed the creation of a service monitor from the cdi-operator.
128 changes: 128 additions & 0 deletions images/virt-artifact/patches/021-support-qcow2-for-filesystem.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
diff --git a/pkg/host-disk/host-disk.go b/pkg/host-disk/host-disk.go
index 7ada596d87..5be737e557 100644
--- a/pkg/host-disk/host-disk.go
+++ b/pkg/host-disk/host-disk.go
@@ -22,6 +22,7 @@ package hostdisk
import (
"fmt"
"os"
+ "os/exec"
"path"
"path/filepath"
"syscall"
@@ -171,6 +172,15 @@ func createSparseRaw(fullPath string, size int64) (err error) {
return nil
}

+func createQcow2(fullPath string, size int64) (err error) {
+ log.Log.Infof("Create %s with qcow2 format", fullPath)
+ cmd := exec.Command("qemu-img", "create", "-f", "qcow2", fullPath, fmt.Sprintf("%db", size))
+ if err = cmd.Run(); err != nil {
+ return fmt.Errorf("failed to create qcow2: %w", err)
+ }
+ return nil
+}
+
func getPVCDiskImgPath(volumeName string, diskName string) string {
return path.Join(pvcBaseDir, volumeName, diskName)
}
@@ -236,7 +246,7 @@ func (hdc *DiskImgCreator) mountHostDiskAndSetOwnership(vmi *v1.VirtualMachineIn
return err
}
if !fileExists {
- if err := hdc.handleRequestedSizeAndCreateSparseRaw(vmi, diskDir, diskPath, hostDisk); err != nil {
+ if err := hdc.handleRequestedSizeAndCreateQcow2(vmi, diskDir, diskPath, hostDisk); err != nil {
return err
}
}
@@ -248,7 +258,7 @@ func (hdc *DiskImgCreator) mountHostDiskAndSetOwnership(vmi *v1.VirtualMachineIn
return nil
}

-func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.VirtualMachineInstance, diskDir string, diskPath string, hostDisk *v1.HostDisk) error {
+func (hdc *DiskImgCreator) handleRequestedSizeAndCreateQcow2(vmi *v1.VirtualMachineInstance, diskDir string, diskPath string, hostDisk *v1.HostDisk) error {
size, err := hdc.dirBytesAvailableFunc(diskDir, hdc.minimumPVCReserveBytes)
availableSize := int64(size)
if err != nil {
@@ -261,9 +271,9 @@ func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.Virtual
return err
}
}
- err = createSparseRaw(diskPath, requestedSize)
+ err = createQcow2(diskPath, requestedSize)
if err != nil {
- log.Log.Reason(err).Errorf("Couldn't create a sparse raw file for disk path: %s, error: %v", diskPath, err)
+ log.Log.Reason(err).Errorf("Couldn't create a qcow2 file for disk path: %s, error: %v", diskPath, err)
return err
}
return nil
diff --git a/pkg/virt-launcher/virtwrap/converter/converter.go b/pkg/virt-launcher/virtwrap/converter/converter.go
index c64b315d8f..71bbe50037 100644
--- a/pkg/virt-launcher/virtwrap/converter/converter.go
+++ b/pkg/virt-launcher/virtwrap/converter/converter.go
@@ -590,7 +590,6 @@ func Add_Agent_To_api_Channel() (channel api.Channel) {
}

func Convert_v1_Volume_To_api_Disk(source *v1.Volume, disk *api.Disk, c *ConverterContext, diskIndex int) error {
-
if source.ContainerDisk != nil {
return Convert_v1_ContainerDiskSource_To_api_Disk(source.Name, source.ContainerDisk, disk, c, diskIndex)
}
@@ -733,7 +732,7 @@ func Convert_v1_Hotplug_DataVolume_To_api_Disk(name string, disk *api.Disk, c *C
// Convert_v1_FilesystemVolumeSource_To_api_Disk takes a FS source and builds the domain Disk representation
func Convert_v1_FilesystemVolumeSource_To_api_Disk(volumeName string, disk *api.Disk, volumesDiscardIgnore []string) error {
disk.Type = "file"
- disk.Driver.Type = "raw"
+ disk.Driver.Type = "qcow2"
disk.Driver.ErrorPolicy = "stop"
disk.Source.File = GetFilesystemVolumePath(volumeName)
if !contains(volumesDiscardIgnore, volumeName) {
@@ -745,7 +744,7 @@ func Convert_v1_FilesystemVolumeSource_To_api_Disk(volumeName string, disk *api.
// Convert_v1_Hotplug_FilesystemVolumeSource_To_api_Disk takes a FS source and builds the KVM Disk representation
func Convert_v1_Hotplug_FilesystemVolumeSource_To_api_Disk(volumeName string, disk *api.Disk, volumesDiscardIgnore []string) error {
disk.Type = "file"
- disk.Driver.Type = "raw"
+ disk.Driver.Type = "qcow2"
disk.Driver.ErrorPolicy = "stop"
if !contains(volumesDiscardIgnore, volumeName) {
disk.Driver.Discard = "unmap"
@@ -779,13 +778,37 @@ func Convert_v1_Hotplug_BlockVolumeSource_To_api_Disk(volumeName string, disk *a
}

func Convert_v1_HostDisk_To_api_Disk(volumeName string, path string, disk *api.Disk) error {
+ file := hostdisk.GetMountedHostDiskPath(volumeName, path)
+ format, err := getFormat(file)
+ if err != nil {
+ return fmt.Errorf("cannot detect host disk format: %w", err)
+ }
disk.Type = "file"
- disk.Driver.Type = "raw"
+ disk.Driver.Type = format
disk.Driver.ErrorPolicy = "stop"
- disk.Source.File = hostdisk.GetMountedHostDiskPath(volumeName, path)
+ disk.Source.File = file
return nil
}

+func getFormat(path string) (string, error) {
+ const (
+ formatQcow2 = "qcow2"
+ formatRaw = "raw"
+ )
+ info, err := os.Stat(path)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return formatQcow2, nil
+ }
+ return "", err
+ }
+ mode := info.Mode()
+ if mode&os.ModeDevice != 0 {
+ return formatRaw, nil
+ }
+ return formatQcow2, nil
+}
+
func Convert_v1_SysprepSource_To_api_Disk(volumeName string, disk *api.Disk) error {
if disk.Type == "lun" {
return fmt.Errorf(deviceTypeNotCompatibleFmt, disk.Alias.GetName())
Loading

0 comments on commit 754c4a7

Please sign in to comment.