Block dev for block uploader backup (#9994)

* add block dev operations for block data mover backup

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

* add block dev operations for block data mover backup

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

* Add block device operations for block uploader backup

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

* Add block device operations for block uploader backup

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

---------

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>
This commit is contained in:
lyndon-li
2026-07-16 13:44:41 +08:00
committed by GitHub
parent dfa0a91c01
commit b0d7ada06a
4 changed files with 412 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
Add block device operations for block uploader backup
+49 -2
View File
@@ -21,11 +21,58 @@ package block
import (
"os"
"path/filepath"
"syscall"
"github.com/cockroachdb/errors"
)
// implement in following PRs
var lstatFunc = os.Lstat
var openFileFunc = os.OpenFile
// openBlockDevice opens a block device for read/write, caller needs to close the returned handle
func openBlockDevice(path string, read bool) (*os.File, error) {
return nil, errors.New("Not implemented")
devPath, err := resolveSymlink(path)
if err != nil {
return nil, errors.Wrap(err, "resolveSymlink")
}
fileInfo, err := lstatFunc(devPath)
if err != nil {
return nil, errors.Wrapf(err, "unable to get the device information %s", devPath)
}
if (fileInfo.Sys().(*syscall.Stat_t).Mode & syscall.S_IFMT) != syscall.S_IFBLK {
return nil, errors.Errorf("path %s is not a block device", devPath)
}
flag := os.O_RDWR
mode := os.FileMode(0666)
if read {
flag = os.O_RDONLY
mode = 0
}
device, err := openFileFunc(devPath, flag|syscall.O_DIRECT, mode)
if err != nil {
if os.IsPermission(err) || errors.Is(err, syscall.EPERM) {
return nil, errors.Wrapf(err, "no permission to open device %s with mode %v", devPath, mode)
}
return nil, errors.Wrapf(err, "unable to open device %s", devPath)
}
return device, nil
}
func resolveSymlink(path string) (string, error) {
st, err := os.Lstat(path)
if err != nil {
return "", errors.Wrap(err, "stat")
}
if (st.Mode() & os.ModeSymlink) == 0 {
return path, nil
}
return filepath.EvalSymlinks(path)
}
+358
View File
@@ -0,0 +1,358 @@
//go:build linux
// +build linux
/*
Copyright The Velero Contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package block
import (
"os"
"path/filepath"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeBlockDevFileInfo struct{}
func (fakeBlockDevFileInfo) Name() string { return "fake-blk" }
func (fakeBlockDevFileInfo) Size() int64 { return 0 }
func (fakeBlockDevFileInfo) Mode() os.FileMode { return os.ModeDevice }
func (fakeBlockDevFileInfo) ModTime() time.Time { return time.Time{} }
func (fakeBlockDevFileInfo) IsDir() bool { return false }
func (fakeBlockDevFileInfo) Sys() any {
return &syscall.Stat_t{Mode: syscall.S_IFBLK}
}
func TestResolveSymlink(t *testing.T) {
testCases := []struct {
name string
setupPath func(t *testing.T) string
expectError bool
errContains string
checkResult func(t *testing.T, input, result string)
}{
{
name: "path does not exist returns error",
setupPath: func(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "nonexistent")
},
expectError: true,
errContains: "stat",
},
{
name: "regular file returns same path",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "regular-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.Equal(t, input, result)
},
},
{
name: "directory returns same path",
setupPath: func(t *testing.T) string {
t.Helper()
return t.TempDir()
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.Equal(t, input, result)
},
},
{
name: "symlink to existing file returns target real path",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
target, err := os.CreateTemp(dir, "target-*")
require.NoError(t, err)
target.Close()
linkPath := filepath.Join(dir, "link")
require.NoError(t, os.Symlink(target.Name(), linkPath))
return linkPath
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.NotEqual(t, input, result)
fi, err := os.Lstat(result)
require.NoError(t, err)
assert.Zero(t, fi.Mode()&os.ModeSymlink)
},
},
{
name: "symlink to existing directory returns resolved path",
setupPath: func(t *testing.T) string {
t.Helper()
outer := t.TempDir()
inner := t.TempDir()
linkPath := filepath.Join(outer, "dirlink")
require.NoError(t, os.Symlink(inner, linkPath))
return linkPath
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.NotEqual(t, input, result)
fi, err := os.Lstat(result)
require.NoError(t, err)
assert.True(t, fi.IsDir())
},
},
{
name: "broken symlink — target does not exist — returns error",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
linkPath := filepath.Join(dir, "broken-link")
require.NoError(t, os.Symlink(filepath.Join(dir, "nonexistent-target"), linkPath))
return linkPath
},
expectError: true,
errContains: "no such file or directory",
},
{
name: "chain of symlinks is fully resolved",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
// real → link1 → link2 (two-hop chain)
real, err := os.CreateTemp(dir, "real-*")
require.NoError(t, err)
real.Close()
link1 := filepath.Join(dir, "link1")
require.NoError(t, os.Symlink(real.Name(), link1))
link2 := filepath.Join(dir, "link2")
require.NoError(t, os.Symlink(link1, link2))
return link2
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.NotEqual(t, input, result)
fi, err := os.Lstat(result)
require.NoError(t, err)
assert.Zero(t, fi.Mode()&os.ModeSymlink)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
input := tc.setupPath(t)
result, err := resolveSymlink(input)
if tc.expectError {
require.Error(t, err)
if tc.errContains != "" {
require.ErrorContains(t, err, tc.errContains)
}
assert.Empty(t, result)
} else {
require.NoError(t, err)
if tc.checkResult != nil {
tc.checkResult(t, input, result)
}
}
})
}
}
func TestOpenBlockDevice(t *testing.T) {
testCases := []struct {
name string
setupPath func(t *testing.T) string
read bool
expectError bool
errContains string
injectLstat func(string) (os.FileInfo, error)
injectOpenFile func(string, int, os.FileMode) (*os.File, error)
}{
{
name: "path does not exist — resolveSymlink fails",
setupPath: func(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "nonexistent")
},
read: true,
expectError: true,
errContains: "resolveSymlink",
},
{
name: "regular file is not a block device — read mode",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "regular-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: true,
expectError: true,
errContains: "is not a block device",
},
{
name: "regular file is not a block device — write mode",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "regular-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: false,
expectError: true,
errContains: "is not a block device",
},
{
name: "directory is not a block device",
setupPath: func(t *testing.T) string {
t.Helper()
return t.TempDir()
},
read: true,
expectError: true,
errContains: "is not a block device",
},
{
name: "symlink to regular file is not a block device",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
target, err := os.CreateTemp(dir, "target-*")
require.NoError(t, err)
target.Close()
linkPath := filepath.Join(dir, "link")
require.NoError(t, os.Symlink(target.Name(), linkPath))
return linkPath
},
read: true,
expectError: true,
errContains: "is not a block device",
},
{
name: "broken symlink — resolveSymlink fails",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
linkPath := filepath.Join(dir, "broken-link")
require.NoError(t, os.Symlink(filepath.Join(dir, "ghost"), linkPath))
return linkPath
},
read: true,
expectError: true,
errContains: "resolveSymlink",
},
{
name: "EACCES from OpenFile — permission denied message",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "blk-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: true,
expectError: true,
errContains: "no permission to open device",
injectLstat: func(_ string) (os.FileInfo, error) {
return fakeBlockDevFileInfo{}, nil
},
injectOpenFile: func(name string, _ int, _ os.FileMode) (*os.File, error) {
t.Helper()
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EACCES}
},
},
{
name: "EPERM from OpenFile — permission denied message",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "blk-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: false,
expectError: true,
errContains: "no permission to open device",
injectLstat: func(_ string) (os.FileInfo, error) {
return fakeBlockDevFileInfo{}, nil
},
injectOpenFile: func(name string, _ int, _ os.FileMode) (*os.File, error) {
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EPERM}
},
},
{
name: "generic OpenFile error — unable to open device message",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "blk-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: true,
expectError: true,
errContains: "unable to open device",
injectLstat: func(_ string) (os.FileInfo, error) {
t.Helper()
return fakeBlockDevFileInfo{}, nil
},
injectOpenFile: func(name string, _ int, _ os.FileMode) (*os.File, error) {
t.Helper()
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EIO}
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
lstatFunc = os.Lstat
openFileFunc = os.OpenFile
})
if tc.injectLstat != nil {
lstatFunc = tc.injectLstat
}
if tc.injectOpenFile != nil {
openFileFunc = tc.injectOpenFile
}
path := tc.setupPath(t)
f, err := openBlockDevice(path, tc.read)
if tc.expectError {
require.Error(t, err)
if tc.errContains != "" {
require.ErrorContains(t, err, tc.errContains)
}
assert.Nil(t, f)
} else {
require.NoError(t, err)
require.NotNil(t, f)
f.Close()
}
})
}
}
+4
View File
@@ -67,6 +67,8 @@ func Backup(ctx context.Context, blkUp Uploader, repoWriter udmrepo.BackupRepo,
return uploader.SnapshotInfo{}, false, errors.Wrapf(err, "error opening block device %s", source)
}
defer sourceInfo.dev.Close()
sourceInfo.size, err = sourceInfo.dev.Seek(0, io.SeekEnd)
if err != nil {
return uploader.SnapshotInfo{}, false, errors.Wrapf(err, "error getting length of block device %s", source)
@@ -218,6 +220,8 @@ func Restore(ctx context.Context, blkUp Uploader, rep udmrepo.BackupRepo, snapsh
return 0, errors.Wrapf(err, "error opening block device '%s'", destPath)
}
defer destDev.Close()
size, err := blkUp.Restore(snapshot, destInfo{dev: destDev, path: destPath}, bitmap.Iterator(), uploaderCfg)
if err != nil {
return 0, errors.Wrapf(err, "error restoring to block dev %s", destPath)