Skip to content

Commit

Permalink
Expose virtual size of qcow2 backing images
Browse files Browse the repository at this point in the history
This commit adds VirtualSize to the BackingImage, BackingImageStatus,
BackingImageSpec and FileInfo structs. This will allow longhorn-manager
to in turn pick up the virtual image size and expose it in the
status section of the BackingImage and BackingImageManager CRDs.

The virtual size of an image is determined by running `qemu-img info`.
For qcow2 images, the virtual size will usually be larger than the
actual physical file size.  For raw images, `qemu-img info` still reports
virtual size, but it's the same as the physical file size in this case.

It's important to note that we can only report the virtual size of an
image once the syncing file is ready.  If the syncing file is not yet
ready (or if for some reason the call to `qemu-img info` fails), virtual
size will be set to zero.

Related issue: longhorn/longhorn#7923

Signed-off-by: Tim Serong <[email protected]>
  • Loading branch information
tserong committed Mar 7, 2024
1 parent 61a69c0 commit a8b08c1
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 218 deletions.
3 changes: 3 additions & 0 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type BackingImage struct {
Name string `json:"name"`
UUID string `json:"uuid"`
Size int64 `json:"size"`
VirtualSize int64 `json:"virtualSize"`
ExpectedChecksum string `json:"expectedChecksum"`

Status BackingImageStatus `json:"status"`
Expand All @@ -32,6 +33,7 @@ func RPCToBackingImage(obj *rpc.BackingImageResponse) *BackingImage {
Name: obj.Spec.Name,
UUID: obj.Spec.Uuid,
Size: obj.Spec.Size,
VirtualSize: obj.Spec.VirtualSize,
ExpectedChecksum: obj.Spec.Checksum,

Status: BackingImageStatus{
Expand Down Expand Up @@ -112,6 +114,7 @@ type FileInfo struct {
FilePath string `json:"filePath"`
UUID string `json:"uuid"`
Size int64 `json:"size"`
VirtualSize int64 `json:"virtualSize"`
State string `json:"state"`
Progress int `json:"progress"`
ProcessedSize int64 `json:"processedSize"`
Expand Down
9 changes: 5 additions & 4 deletions pkg/manager/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,11 @@ func (m *Manager) listAndUpdate() (biFileInfoMap map[string]*api.FileInfo, err e
func backingImageResponse(fInfo *api.FileInfo) *rpc.BackingImageResponse {
return &rpc.BackingImageResponse{
Spec: &rpc.BackingImageSpec{
Name: types.GetBackingImageNameFromFilePath(fInfo.FilePath, fInfo.UUID),
Uuid: fInfo.UUID,
Size: fInfo.Size,
Checksum: fInfo.ExpectedChecksum,
Name: types.GetBackingImageNameFromFilePath(fInfo.FilePath, fInfo.UUID),
Uuid: fInfo.UUID,
Size: fInfo.Size,
VirtualSize: fInfo.VirtualSize,
Checksum: fInfo.ExpectedChecksum,
},
Status: &rpc.BackingImageStatus{
State: fInfo.State,
Expand Down
438 changes: 224 additions & 214 deletions pkg/rpc/rpc.pb.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pkg/rpc/rpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ message BackingImageSpec {
string uuid = 2;
int64 size = 3;
string checksum = 4;
int64 virtualSize = 5;
}

message BackingImageStatus {
Expand Down
114 changes: 114 additions & 0 deletions pkg/sync/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,96 @@ func (s *SyncTestSuite) TestReadyFileValidation(c *C) {
c.Assert(err, IsNil)
}

func (s *SyncTestSuite) TestVirtualSizeQcow2(c *C) {
logrus.Debugf("Testing sync server: TestVirtualSizeQcow2")

fileName := "sync-virtual-size-qcow2"
curPath := filepath.Join(s.dir, fileName)
sizeInGB := int64(20)

logrus.Debugf("creating %dG qcow2 file %v", sizeInGB, curPath)
err := createQcow2File(curPath, sizeInGB)
c.Assert(err, IsNil)

checksum, err := util.GetFileChecksum(curPath)
c.Assert(err, IsNil)
c.Assert(checksum, Not(Equals), "")

stat, err := os.Stat(curPath)
c.Assert(err, IsNil)
fileSize := stat.Size()

go func() {
_ = NewServer(s.ctx, s.addr, &MockHandler{})
}()
isRunning := util.DetectHTTPServerAvailability(s.httpAddr, 5, true)
c.Assert(isRunning, Equals, true)

cli := &client.SyncClient{
Remote: s.addr,
}

err = cli.Fetch(curPath, curPath, TestSyncingFileUUID, TestDiskUUID, checksum, fileSize)
c.Assert(err, IsNil)
fInfo, err := getAndWaitFileState(cli, curPath, string(types.StateReady), 1)
c.Assert(err, IsNil)

// With a qcow2 file, the actual file size should be less
// than the virtual size, and both file size and virtual
// size should be correctly reported in the FileInfo struct
c.Assert(fInfo.Size < fInfo.VirtualSize, Equals, true)
c.Assert(fInfo.Size, Equals, fileSize)
c.Assert(fInfo.VirtualSize, Equals, sizeInGB*1024*1024*1024)
c.Assert(fInfo.Size, Not(Equals), fInfo.VirtualSize)

err = cli.Delete(curPath)
c.Assert(err, IsNil)
}

func (s *SyncTestSuite) TestVirtualSizeRaw(c *C) {
logrus.Debugf("Testing sync server: TestVirtualSizeRaw")

fileName := "sync-virtual-size-raw"
curPath := filepath.Join(s.dir, fileName)
sizeInMB := int64(1)

logrus.Debugf("creating %dM raw file %v", sizeInMB, curPath)
err := generateRandomDataFile(curPath, strconv.FormatInt(sizeInMB, 10))
c.Assert(err, IsNil)

checksum, err := util.GetFileChecksum(curPath)
c.Assert(err, IsNil)
c.Assert(checksum, Not(Equals), "")

stat, err := os.Stat(curPath)
c.Assert(err, IsNil)
fileSize := stat.Size()

go func() {
_ = NewServer(s.ctx, s.addr, &MockHandler{})
}()
isRunning := util.DetectHTTPServerAvailability(s.httpAddr, 5, true)
c.Assert(isRunning, Equals, true)

cli := &client.SyncClient{
Remote: s.addr,
}

err = cli.Fetch(curPath, curPath, TestSyncingFileUUID, TestDiskUUID, checksum, fileSize)
c.Assert(err, IsNil)
fInfo, err := getAndWaitFileState(cli, curPath, string(types.StateReady), 1)
c.Assert(err, IsNil)

// With a raw file, the actual file size and the
// virtual size should be equal, and both should
// be correctly reported in the FileInfo struct
c.Assert(fInfo.Size, Equals, fileSize)
c.Assert(fInfo.VirtualSize, Equals, fileSize)

err = cli.Delete(curPath)
c.Assert(err, IsNil)
}

func getAndWaitFileState(cli *client.SyncClient, curPath, desireState string, waitIntervalInSecond int) (fInfo *api.FileInfo, err error) {
endTime := time.Now().Add(time.Duration(waitIntervalInSecond) * time.Second)

Expand Down Expand Up @@ -679,6 +769,7 @@ func getAndWaitFileState(cli *client.SyncClient, curPath, desireState string, wa
FilePath: fInfo.FilePath,
UUID: fInfo.UUID,
Size: fInfo.Size,
VirtualSize: fInfo.VirtualSize,
ExpectedChecksum: fInfo.ExpectedChecksum,
CurrentChecksum: fInfo.CurrentChecksum,
ModificationTime: fInfo.ModificationTime,
Expand Down Expand Up @@ -731,3 +822,26 @@ func generateRandomDataFile(filePath, sizeInMB string) error {

return exec.Command("dd", "if=/dev/urandom", "of="+filePath, "bs=1M", "count="+sizeInMB).Run()
}

func createQcow2File(filePath string, sizeInGB int64) error {
// A simple call to `qemu-img create -f qcow2 $filePath 20G` gives
// us a file that's only 196928 bytes on disk. Unfortunately, this is
// *not* an even multiple of 512 bytes (196928 / 512 == 384.625), which
// means when we use it as sync file we hit an error: "the file size
// 196928 should be a multiple of 512 bytes since Longhorn uses directIO
// by default". The least worst workaround for this I could think of
// is using `dd` to stick an extra 512 byte block of zeros on the end
// if the file size isn't evenly divisible by 512.
err := exec.Command(util.QemuImgBinary, "create", "-f", "qcow2", filePath, strconv.FormatInt(sizeInGB, 10)+"G").Run()
if err != nil {
return err
}
stat, err := os.Stat(filePath)
if err != nil {
return err
}
if stat.Size()%512 == 0 {
return nil
}
return exec.Command("dd", "if=/dev/zero", "of="+filePath, "bs=512", "count=1", "seek="+strconv.FormatInt(stat.Size()/512+1, 10)).Run()
}
16 changes: 16 additions & 0 deletions pkg/sync/sync_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type SyncingFile struct {
uuid string
diskUUID string
size int64
virtualSize int64
state types.State
progress int
processedSize int64
Expand Down Expand Up @@ -252,6 +253,7 @@ func (sf *SyncingFile) checkAndReuseFile() (err error) {
sf.processedSize = info.Size()
sf.modificationTime = info.ModTime().UTC().String()
sf.updateSyncReadyNoLock()
sf.updateVirtualSizeNoLock(sf.filePath)
sf.writeConfigNoLock()
sf.lock.Unlock()

Expand Down Expand Up @@ -356,6 +358,7 @@ func (sf *SyncingFile) getNoLock() api.FileInfo {
FilePath: sf.filePath,
UUID: sf.uuid,
Size: sf.size,
VirtualSize: sf.virtualSize,
State: string(sf.state),
Progress: sf.progress,
ProcessedSize: sf.processedSize,
Expand Down Expand Up @@ -743,6 +746,7 @@ func (sf *SyncingFile) finishProcessing(err error) (finalErr error) {
logrus.Debugf("SyncingFile: directly get the checksum from the valid config during processing wrap-up: %v", config.CurrentChecksum)
sf.currentChecksum = config.CurrentChecksum
sf.updateSyncReadyNoLock()
sf.updateVirtualSizeNoLock(sf.tmpFilePath)
sf.writeConfigNoLock()

// Renaming won't change the file modification time.
Expand Down Expand Up @@ -791,6 +795,7 @@ func (sf *SyncingFile) postProcessSyncFile() {
return
}
sf.updateSyncReadyNoLock()
sf.updateVirtualSizeNoLock(sf.tmpFilePath)
sf.writeConfigNoLock()

// Renaming won't change the file modification time.
Expand All @@ -812,6 +817,16 @@ func (sf *SyncingFile) updateSyncReadyNoLock() {
})
}

func (sf *SyncingFile) updateVirtualSizeNoLock(filePath string) {
// This only works if filePath is valid - sometimes we need to call it
// with sf.tmpFilePath, sometimes with sf.filePath :-/
virtualSize, err := util.GetImageVirtualSize(filePath)
if err != nil {
sf.log.Warnf("SyncingFile: failed to get backing image virtual size: %v", err)
}
sf.virtualSize = virtualSize // This will be zero in case of error. Not sure if that's what we want or not.
}

func (sf *SyncingFile) handleFailureNoLock(err error) {
if err == nil {
return
Expand Down Expand Up @@ -840,6 +855,7 @@ func (sf *SyncingFile) writeConfigNoLock() {
FilePath: sf.filePath,
UUID: sf.uuid,
Size: sf.size,
VirtualSize: sf.virtualSize,
ExpectedChecksum: sf.expectedChecksum,
CurrentChecksum: sf.currentChecksum,
ModificationTime: sf.modificationTime,
Expand Down
21 changes: 21 additions & 0 deletions pkg/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ type SyncingFileConfig struct {
FilePath string `json:"name"`
UUID string `json:"uuid"`
Size int64 `json:"size"`
VirtualSize int64 `json:"virtualSize"`
ExpectedChecksum string `json:"expectedChecksum"`
CurrentChecksum string `json:"currentChecksum"`
ModificationTime string `json:"modificationTime"`
Expand Down Expand Up @@ -270,6 +271,26 @@ func DetectFileFormat(filePath string) (string, error) {
return "", fmt.Errorf("cannot find the file format in the output %s", output)
}

func GetImageVirtualSize(filePath string) (int64, error) {
type qemuImgInfo struct {
VirtualSize int64 `json:"virtual-size"`
}

output, err := Execute([]string{}, QemuImgBinary, "info", "--output=json", filePath)
if err != nil {
return 0, err
}

var q qemuImgInfo
err = json.Unmarshal([]byte(output), &q)
if err != nil {
return 0, err
}

// If it's a raw file, `qemu-img info` will return virtual size == size
return q.VirtualSize, nil
}

func ConvertFromRawToQcow2(filePath string) error {
if format, err := DetectFileFormat(filePath); err != nil {
return err
Expand Down

0 comments on commit a8b08c1

Please sign in to comment.