Automated cherry pick of #24012: feat(host): mount post overlay layers by shortest path order (#24018)

* fix(host): mount and umount order for post overlay

- support host_lower_map for post overlay image

* feat(host): tar dirs of pod with include patterns
This commit is contained in:
Zexi Li
2026-01-06 20:03:41 +08:00
committed by GitHub
parent aaeb09fb72
commit 9fbad19989
17 changed files with 297 additions and 44 deletions
+1
View File
@@ -126,6 +126,7 @@ type DiskBackupDetails struct {
type DiskBackupAsTarInput struct {
IncludeFiles []string `json:"include_files"`
IncludePatterns []string `json:"include_patterns"`
ExcludeFiles []string `json:"exclude_files"`
ContainerId string `json:"container_id"`
IgnoreNotExistFile bool `json:"ignore_not_exist_file"`
+2 -1
View File
@@ -218,7 +218,8 @@ type ContainerSaveVolumeMountToImageInput struct {
Dirs []string `json:"dirs"`
UsedByPostOverlay bool `json:"used_by_post_overlay"`
DirPrefix string `json:"dir_prefix"`
DirPrefix string `json:"dir_prefix"`
ExcludePaths []string `json:"exclude_paths"`
}
type ContainerExecInfoOutput struct {
+9
View File
@@ -262,9 +262,16 @@ func (o ContainerVolumeMountDiskOverlay) IsValid() error {
return nil
}
type HostLowerPath struct {
PrePath string `json:"pre_path"`
PostPath string `json:"post_path"`
}
type ContainerVolumeMountDiskPostImageOverlay struct {
Id string `json:"id"`
PathMap map[string]string `json:"path_map"`
// 宿主机底层目录映射, key 为 PathMap 的 key,value 为 overlay lower 格式,多目录以 ":" 分隔
HostLowerMap map[string]*HostLowerPath `json:"host_lower_map"`
}
type ContainerVolumeMountDiskPostImageOverlayUnpacker ContainerVolumeMountDiskPostImageOverlay
@@ -277,6 +284,7 @@ func (ov *ContainerVolumeMountDiskPostImageOverlay) UnmarshalJSON(data []byte) e
ov.Id = nov.Id
// 防止 PathMap 被合并,总是用 Unarmshal data 里面的 path_map
ov.PathMap = nov.PathMap
ov.HostLowerMap = nov.HostLowerMap
return nil
}
@@ -295,6 +303,7 @@ type ContainerVolumeMountDiskPostOverlay struct {
Image *ContainerVolumeMountDiskPostImageOverlay `json:"image"`
FsUser *int64 `json:"fs_user,omitempty"`
FsGroup *int64 `json:"fs_group,omitempty"`
FlattenLayers bool `json:"flatten_layers"`
}
func (o ContainerVolumeMountDiskPostOverlay) IsEqual(input ContainerVolumeMountDiskPostOverlay) bool {
+2 -1
View File
@@ -147,7 +147,8 @@ type ContainerSaveVolumeMountToImageInput struct {
VolumeMount *ContainerVolumeMount `json:"volume_mount"`
VolumeMountDirs []string `json:"volume_mount_dirs"`
VolumeMountPrefix string `json:"volume_mount_prefix"`
VolumeMountPrefix string `json:"volume_mount_prefix"`
ExcludePaths []string `json:"exclude_paths"`
}
type ContainerCommitInput struct {
@@ -71,6 +71,14 @@ func (p povImage) validateData(ctx context.Context, userCred mcclient.TokenCrede
if len(pov.Image.PathMap) == 0 {
pov.Image.PathMap = pathMap
}
if len(pov.Image.HostLowerMap) != 0 {
for hostPath, _ := range pov.Image.HostLowerMap {
_, ok := pov.Image.PathMap[hostPath]
if !ok {
return httperrors.NewNotFoundError("host_path %s of host_lower_map doesn't found in path_map", hostPath)
}
}
}
return nil
}
+8
View File
@@ -29,6 +29,7 @@ import (
"yunion.io/x/pkg/util/fileutils"
"yunion.io/x/pkg/util/osprofile"
"yunion.io/x/pkg/util/regutils"
"yunion.io/x/pkg/utils"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
@@ -716,3 +717,10 @@ func (base *SBaseGuestDriver) BeforeAttachIsolatedDevice(ctx context.Context, cr
func (base *SBaseGuestDriver) RequestUploadGuestStatus(ctx context.Context, guest *models.SGuest, task taskman.ITask) error {
return errors.Wrapf(cloudprovider.ErrNotImplemented, "RequestUploadGuestStatus")
}
func (base *SBaseGuestDriver) CanStop(guest *models.SGuest) error {
if utils.IsInStringArray(guest.Status, []string{api.VM_RUNNING, api.VM_STOP_FAILED, api.POD_STATUS_CRASH_LOOP_BACK_OFF, api.POD_STATUS_CONTAINER_EXITED, api.VM_KICKSTART_INSTALLING, api.VM_KICKSTART_FAILED, api.VM_KICKSTART_COMPLETED}) {
return nil
}
return errors.Wrapf(errors.ErrInvalidStatus, "Cannot stop server in status %s", guest.Status)
}
+7
View File
@@ -329,6 +329,13 @@ func (p *SPodDriver) OnGuestDeployTaskDataReceived(ctx context.Context, guest *m
return nil
}
func (p *SPodDriver) CanStop(guest *models.SGuest) error {
if guest.PowerStates == api.VM_POWER_STATES_ON {
return nil
}
return p.SKVMGuestDriver.CanStop(guest)
}
func (p *SPodDriver) StartGuestStopTask(guest *models.SGuest, ctx context.Context, userCred mcclient.TokenCredential, params *jsonutils.JSONDict, parentTaskId string) error {
task, err := taskman.TaskManager.NewTask(ctx, "PodStopTask", guest, userCred, params, parentTaskId, "", nil)
if err != nil {
+7 -1
View File
@@ -482,10 +482,15 @@ func (c *SContainer) StartCreateTask(ctx context.Context, userCred mcclient.Toke
return task.ScheduleRun(nil)
}
func (c *SContainer) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input *api.ContainerUpdateInput) (*api.ContainerUpdateInput, error) {
// func (c *SContainer) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input *api.ContainerUpdateInput) (*api.ContainerUpdateInput, error) {
func (c *SContainer) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (*api.ContainerUpdateInput, error) {
if !api.ContainerExitedStatus.Has(c.GetStatus()) {
return nil, httperrors.NewInvalidStatusError("current status %s is not in %v", c.GetStatus(), api.ContainerExitedStatus.List())
}
input := new(api.ContainerUpdateInput)
if err := data.Unmarshal(input); err != nil {
return nil, errors.Wrap(err, "Unmarshal")
}
baseInput, err := c.SVirtualResourceBase.ValidateUpdateData(ctx, userCred, query, input.VirtualResourceBaseUpdateInput)
if err != nil {
@@ -968,6 +973,7 @@ func (c *SContainer) PerformSaveVolumeMountImage(ctx context.Context, userCred m
VolumeMountDirs: cleanupDirPaths(input.Dirs),
VolumeMountPrefix: cleanupDirPath(input.DirPrefix),
ExcludePaths: cleanupDirPaths(input.ExcludePaths),
}
return hostInput, c.StartSaveVolumeMountImage(ctx, userCred, hostInput, "")
+13 -7
View File
@@ -3857,14 +3857,20 @@ func (self *SGuest) PerformStatus(ctx context.Context, userCred mcclient.TokenCr
// 关机
func (self *SGuest) PerformStop(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject,
input api.ServerStopInput) (jsonutils.JSONObject, error) {
// XXX if is force, force stop guest
if input.IsForce || utils.IsInStringArray(self.Status, []string{api.VM_RUNNING, api.VM_STOP_FAILED, api.POD_STATUS_CRASH_LOOP_BACK_OFF, api.POD_STATUS_CONTAINER_EXITED, api.VM_KICKSTART_INSTALLING, api.VM_KICKSTART_FAILED, api.VM_KICKSTART_COMPLETED}) {
if err := self.ValidateEncryption(ctx, userCred); err != nil {
return nil, errors.Wrap(httperrors.ErrForbidden, "encryption key not accessible")
}
return nil, self.StartGuestStopTask(ctx, userCred, input.TimeoutSecs, input.IsForce, input.StopCharging, "")
drv, err := self.GetDriver()
if err != nil {
return nil, errors.Wrap(err, "GetDriver")
}
return nil, httperrors.NewInvalidStatusError("Cannot stop server in status %s", self.Status)
if !input.IsForce {
err := drv.CanStop(self)
if err != nil {
return nil, errors.Wrap(err, "Cannot stop server")
}
}
if err := self.ValidateEncryption(ctx, userCred); err != nil {
return nil, errors.Wrap(httperrors.ErrForbidden, "encryption key not accessible")
}
return nil, self.StartGuestStopTask(ctx, userCred, input.TimeoutSecs, input.IsForce, input.StopCharging, "")
}
// 冻结虚拟机
+1
View File
@@ -84,6 +84,7 @@ type IGuestDriver interface {
RequestGuestCreateInsertIso(ctx context.Context, imageId string, bootIndex *int8, task taskman.ITask, guest *SGuest) error
CanStop(guest *SGuest) error
StartGuestStopTask(guest *SGuest, ctx context.Context, userCred mcclient.TokenCredential, params *jsonutils.JSONDict, parentTaskId string) error
StartGuestResetTask(guest *SGuest, ctx context.Context, userCred mcclient.TokenCredential, isHard bool, parentTaskId string) error
StartGuestRestartTask(guest *SGuest, ctx context.Context, userCred mcclient.TokenCredential, isForce bool, parentTaskId string) error
+1 -1
View File
@@ -88,7 +88,7 @@ func UnmountWithSubDirs(devPath string) error {
return err
}
// found mountpoints starts with devPath
out, err2 := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", fmt.Sprintf("mount | grep ' %s/' | awk '{print $3}'", devPath)).Output()
out, err2 := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", fmt.Sprintf("mount | grep ' %s/' | awk '{print $3}' | tac", devPath)).Output()
if err2 == nil && len(out) != 0 {
mntPoints := strings.Split(string(out), "\n")
for _, mntPoint := range mntPoints {
+45 -15
View File
@@ -185,6 +185,39 @@ func (d disk) mountDisk(devPath string, mntPoint string, fs string, resUid int,
return nil
}
func (d disk) Connect(pod volume_mount.IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount) (string, bool, error) {
iDisk, _, err := d.getPodDisk(pod, vm)
if err != nil {
return "", false, errors.Wrap(err, "get pod disk interface")
}
return d.connectDisk(iDisk)
}
func (d disk) Disconnect(pod volume_mount.IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount) error {
iDisk, _, err := d.getPodDisk(pod, vm)
if err != nil {
return errors.Wrap(err, "get pod disk interface")
}
drv, err := iDisk.GetContainerStorageDriver()
if err != nil {
return errors.Wrap(err, "get disk storage driver")
}
mntPoint := pod.GetDiskMountPoint(iDisk)
_, isConnected, err := drv.CheckConnect(iDisk.GetPath())
if err != nil {
return errors.Wrapf(err, "CheckConnect %s", iDisk.GetPath())
}
if isConnected {
if err := drv.DisconnectDisk(iDisk.GetPath(), mntPoint); err != nil {
return errors.Wrapf(err, "DisconnectDisk %s %s", iDisk.GetPath(), mntPoint)
}
}
if err := volume_mount.RemoveDir(mntPoint); err != nil {
return errors.Wrapf(err, "remove dir %s", mntPoint)
}
return nil
}
func (d disk) connectDiskAndMount(drv container_storage.IContainerStorage, pod volume_mount.IPodInfo, iDisk storageman.IDisk, fs string, resUid, resGid int) (string, error) {
devPath, isConnected, err := d.connectDisk(iDisk)
if err != nil {
@@ -282,15 +315,19 @@ func (d disk) createStorageSizeFile(iDisk storageman.IDisk, mntPoint string, inp
return nil
}
func (d disk) UnmountWithoutDisconnect(pod volume_mount.IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount) error {
return d.unmount(pod, ctrId, vm, false)
}
func (d disk) Unmount(pod volume_mount.IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount) error {
return d.unmount(pod, ctrId, vm, true)
}
func (d disk) unmount(pod volume_mount.IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount, disconnect bool) error {
iDisk, _, err := d.getPodDisk(pod, vm)
if err != nil {
return errors.Wrap(err, "get pod disk interface")
}
drv, err := iDisk.GetContainerStorageDriver()
if err != nil {
return errors.Wrap(err, "get disk storage driver")
}
if len(vm.Disk.PostOverlay) != 0 {
if err := d.UnmountPostOverlays(pod, ctrId, vm, vm.Disk.PostOverlay, false, false); err != nil {
return errors.Wrap(err, "umount post overlay dirs")
@@ -303,20 +340,13 @@ func (d disk) Unmount(pod volume_mount.IPodInfo, ctrId string, vm *hostapi.Conta
}
mntPoint := pod.GetDiskMountPoint(iDisk)
if err := container_storage.UnmountWithSubDirs(mntPoint); err != nil {
return errors.Wrapf(err, "unmount %s", mntPoint)
return errors.Wrapf(err, "UnmountWithSubDirs %s", mntPoint)
}
_, isConnected, err := drv.CheckConnect(iDisk.GetPath())
if err != nil {
return errors.Wrapf(err, "CheckConnect %s", iDisk.GetPath())
}
if isConnected {
if err := drv.DisconnectDisk(iDisk.GetPath(), mntPoint); err != nil {
return errors.Wrapf(err, "DisconnectDisk %s %s", iDisk.GetPath(), mntPoint)
if disconnect {
if err := d.Disconnect(pod, ctrId, vm); err != nil {
return errors.Wrapf(err, "disconnect disk")
}
}
if err := volume_mount.RemoveDir(mntPoint); err != nil {
return errors.Wrapf(err, "remove dir %s", mntPoint)
}
return nil
}
@@ -15,9 +15,13 @@
package disk
import (
"math"
"path/filepath"
"sort"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis"
@@ -97,8 +101,34 @@ func (d diskPostOverlay) getDriver(ov *apis.ContainerVolumeMountDiskPostOverlay)
return getPostOverlayDriver(ov.GetType())
}
// getMinPathMapKeyLength 获取 PathMap 中最短 key 的长度
// 如果 Image 为 nil 或 PathMap 为空,返回一个很大的值,确保排在后面
func getMinPathMapKeyLength(ov *apis.ContainerVolumeMountDiskPostOverlay) int {
if ov.Image == nil || len(ov.Image.PathMap) == 0 {
return math.MaxInt // 返回一个很大的值,确保没有 PathMap 的排在后面
}
minLen := math.MaxInt
for key := range ov.Image.PathMap {
if len(key) < minLen {
minLen = len(key)
}
}
return minLen
}
func sortPostOverlayByPathMapLength(ovs []*apis.ContainerVolumeMountDiskPostOverlay) {
sort.Slice(ovs, func(i, j int) bool {
lenI := getMinPathMapKeyLength(ovs[i])
lenJ := getMinPathMapKeyLength(ovs[j])
return lenI < lenJ
})
}
func (d diskPostOverlay) mountPostOverlays(pod volume_mount.IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount, ovs []*apis.ContainerVolumeMountDiskPostOverlay) error {
// 根据 PathMap 中最短路径长度排序,路径短的排在前面
sortPostOverlayByPathMapLength(ovs)
for _, ov := range ovs {
log.Debugf("mount container %s post overlay dir: %s", ctrId, jsonutils.Marshal(ov).PrettyString())
if err := d.getDriver(ov).Mount(d, pod, ctrId, vm, ov); err != nil {
return errors.Wrapf(err, "mount container %s post overlay dir: %#v", ctrId, ov)
}
@@ -107,7 +137,11 @@ func (d diskPostOverlay) mountPostOverlays(pod volume_mount.IPodInfo, ctrId stri
}
func (d diskPostOverlay) unmountPostOverlays(pod volume_mount.IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount, ovs []*apis.ContainerVolumeMountDiskPostOverlay, useLazy bool, clearLayers bool) error {
for _, ov := range ovs {
//(与 mount 顺序相反)
sortPostOverlayByPathMapLength(ovs)
for i := len(ovs) - 1; i >= 0; i-- {
ov := ovs[i]
log.Debugf("-- unmount container %s post overlay dir: %s", ctrId, jsonutils.Marshal(ov).PrettyString())
if err := d.getDriver(ov).Unmount(d, pod, ctrId, vm, ov, useLazy, clearLayers); err != nil {
return errors.Wrapf(err, "unmount container %s post overlay dir: %#v", ctrId, ov)
}
@@ -128,6 +162,9 @@ func (d diskPostOverlay) getPostOverlayDirWithPrefix(
}
workDir := filepath.Join(rootPath, ov.ContainerTargetDir)
if ov.FlattenLayers {
workDir = filepath.Join(rootPath, strings.ReplaceAll(ov.ContainerTargetDir, "/", "_"))
}
if ensure {
if err := volume_mount.EnsureDir(workDir); err != nil {
return "", errors.Wrapf(err, "makeDir %s", workDir)
@@ -17,6 +17,7 @@ package disk
import (
"context"
"path/filepath"
"strings"
"yunion.io/x/pkg/errors"
@@ -44,25 +45,57 @@ func (i postOverlayImage) getImageInput(ov *apis.ContainerVolumeMountDiskPostOve
return ov.Image
}
func (i postOverlayImage) getCachedImagePaths(d diskPostOverlay, pod volume_mount.IPodInfo, img *apis.ContainerVolumeMountDiskPostImageOverlay, accquire bool) (map[string]string, error) {
func (i postOverlayImage) getCachedImagePaths(d diskPostOverlay, pod volume_mount.IPodInfo, img *apis.ContainerVolumeMountDiskPostImageOverlay, accquire bool) (map[string]string, map[string]*apis.HostLowerPath, error) {
cachedImgDir, err := d.disk.getCachedImageDir(context.Background(), pod, img.Id, accquire)
if err != nil {
return nil, errors.Wrap(err, "disk.getCachedImageDir")
return nil, nil, errors.Wrap(err, "disk.getCachedImageDir")
}
hostLowerMap := make(map[string]*apis.HostLowerPath)
if img.HostLowerMap != nil {
hostLowerMap = img.HostLowerMap
}
result := make(map[string]string)
lowerResult := make(map[string]*apis.HostLowerPath)
for hostPathSuffix, ctrPath := range img.PathMap {
hostPath := filepath.Join(cachedImgDir, hostPathSuffix)
result[hostPath] = ctrPath
hostLowerPath := hostLowerMap[hostPathSuffix]
if hostLowerPath != nil {
lowerResult[hostPath] = hostLowerPath
}
}
return result, nil
return result, lowerResult, nil
}
func (i postOverlayImage) convertToDiskOV(ov *apis.ContainerVolumeMountDiskPostOverlay, hostPath, ctrPath string) *apis.ContainerVolumeMountDiskPostOverlay {
// parseColonSeparatedPaths 解析冒号分隔的路径字符串,过滤空字符串并返回路径列表
func parseColonSeparatedPaths(pathStr string) []string {
var paths []string
for _, path := range strings.Split(pathStr, ":") {
if path != "" {
paths = append(paths, path)
}
}
return paths
}
func (i postOverlayImage) convertToDiskOV(ov *apis.ContainerVolumeMountDiskPostOverlay, hostPath, ctrPath string, hostLowerPath *apis.HostLowerPath) *apis.ContainerVolumeMountDiskPostOverlay {
hostLowerDir := []string{}
if hostLowerPath != nil {
// 添加 PrePath 中的路径
hostLowerDir = append(hostLowerDir, parseColonSeparatedPaths(hostLowerPath.PrePath)...)
}
// 添加当前 hostPath
hostLowerDir = append(hostLowerDir, hostPath)
if hostLowerPath != nil {
// 添加 PostPath 中的路径
hostLowerDir = append(hostLowerDir, parseColonSeparatedPaths(hostLowerPath.PostPath)...)
}
return &apis.ContainerVolumeMountDiskPostOverlay{
HostLowerDir: []string{hostPath},
HostLowerDir: hostLowerDir,
ContainerTargetDir: ctrPath,
FsUser: ov.FsUser,
FsGroup: ov.FsGroup,
FlattenLayers: ov.FlattenLayers,
}
}
@@ -71,12 +104,13 @@ func (i postOverlayImage) withAction(
af func(iDiskPostOverlayDriver, *apis.ContainerVolumeMountDiskPostOverlay) error,
accquire bool) error {
img := i.getImageInput(ov)
paths, err := i.getCachedImagePaths(d, pod, img, accquire)
paths, hostLowerPaths, err := i.getCachedImagePaths(d, pod, img, accquire)
if err != nil {
return errors.Wrapf(err, "get cached image paths")
}
for hostPath, ctrPath := range paths {
dov := i.convertToDiskOV(ov, hostPath, ctrPath)
hostLowerPath := hostLowerPaths[hostPath]
dov := i.convertToDiskOV(ov, hostPath, ctrPath, hostLowerPath)
drv := d.getDriver(dov)
if err := af(drv, dov); err != nil {
return errors.Wrapf(err, "host path %s to %s", hostPath, ctrPath)
@@ -58,6 +58,14 @@ type IVolumeMount interface {
Unmount(pod IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount) error
}
type IConnectedVolumeMount interface {
IVolumeMount
UnmountWithoutDisconnect(pod IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount) error
Connect(pod IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount) (string, bool, error)
Disconnect(pod IPodInfo, ctrId string, vm *hostapi.ContainerVolumeMount) error
}
type ContainerVolumeMountUsage struct {
Id string
HostPath string
+102 -10
View File
@@ -691,12 +691,12 @@ func (s *sPodGuestInstance) umountRootFs(ctrId string, rootFs *hostapi.Container
if rootFs == nil {
return nil
}
drv := volume_mount.GetDriver(rootFs.Type)
drv := volume_mount.GetDriver(rootFs.Type).(volume_mount.IConnectedVolumeMount)
vol := s.ConvertRootFsToVolumeMount(rootFs)
if err := s.clearContainerRootFs(ctrId, rootFs); err != nil {
return errors.Wrapf(err, "clear rootfs of container %s", ctrId)
}
if err := drv.Unmount(s, ctrId, vol); err != nil {
if err := drv.UnmountWithoutDisconnect(s, ctrId, vol); err != nil {
return errors.Wrapf(err, "unmount root fs %s, ctrId %s", jsonutils.Marshal(rootFs), ctrId)
}
return nil
@@ -747,6 +747,30 @@ func (s *sPodGuestInstance) mountPodVolumes() error {
}
func (s *sPodGuestInstance) umountPodVolumes() error {
disConnectFuncs := make([]func() error, 0)
for ctrId, vols := range s.getContainerVolumeMounts() {
for _, vol := range vols {
drv := volume_mount.GetDriver(vol.Type)
connectedDrv, ok := drv.(volume_mount.IConnectedVolumeMount)
if !ok {
if err := drv.Unmount(s, ctrId, vol); err != nil {
return errors.Wrapf(err, "Unmount volume %s, ctrId %s", jsonutils.Marshal(vol), ctrId)
}
} else {
if err := connectedDrv.UnmountWithoutDisconnect(s, ctrId, vol); err != nil {
return errors.Wrapf(err, "UnmountWithoutDisconnect volume %s, ctrId %s", jsonutils.Marshal(vol), ctrId)
}
tmpCtrId := ctrId
tmpVol := vol
disConnectFuncs = append(disConnectFuncs, func() error {
if err := connectedDrv.Disconnect(s, tmpCtrId, tmpVol); err != nil {
return errors.Wrapf(err, "Disconnect volume %s, ctrId %s", jsonutils.Marshal(tmpVol), tmpCtrId)
}
return nil
})
}
}
}
for _, ctr := range s.GetDesc().Containers {
if ctr.Spec.Rootfs == nil {
continue
@@ -755,11 +779,9 @@ func (s *sPodGuestInstance) umountPodVolumes() error {
return errors.Wrapf(err, "umount root fs %s, ctrId %s", jsonutils.Marshal(ctr.Spec.Rootfs), ctr.Id)
}
}
for ctrId, vols := range s.getContainerVolumeMounts() {
for _, vol := range vols {
if err := volume_mount.GetDriver(vol.Type).Unmount(s, ctrId, vol); err != nil {
return errors.Wrapf(err, "Unmount volume %s, ctrId %s", jsonutils.Marshal(vol), ctrId)
}
for _, disConnectFunc := range disConnectFuncs {
if err := disConnectFunc(); err != nil {
return errors.Wrapf(err, "disconnect volume")
}
}
return nil
@@ -2616,7 +2638,38 @@ func (s *sPodGuestInstance) tarGzDir(input *hostapi.ContainerSaveVolumeMountToIm
}
}
cmd := fmt.Sprintf("tar -czf %s -C %s %s", outputFp, hostPath, dirPath)
// 减去 excludePaths 的大小
for _, exclude := range input.ExcludePaths {
// exclude 路径是相对于 hostPath 的
sizeCmd := fmt.Sprintf("du -sb '%s' 2>/dev/null | awk '{print $1}'", exclude)
cmd := fmt.Sprintf("cd %s && %s", hostPath, sizeCmd)
out, err := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", cmd).Output()
if err != nil {
// exclude 路径不存在时忽略错误,继续处理下一个
continue
}
outStr := strings.TrimSpace(string(out))
if outStr == "" {
continue
}
excludeSize, err := strconv.ParseInt(outStr, 10, 64)
if err != nil {
// 解析失败时忽略,继续处理下一个
continue
}
totalSize -= excludeSize
// 确保 totalSize 不为负数
if totalSize < 0 {
totalSize = 0
}
}
baseCmd := "tar -czf"
// 添加 exclude 选项
for _, exclude := range input.ExcludePaths {
baseCmd = fmt.Sprintf("%s --exclude='%s'", baseCmd, exclude)
}
cmd := fmt.Sprintf("%s %s -C %s %s", baseCmd, outputFp, hostPath, dirPath)
if input.VolumeMountPrefix != "" {
cmd += fmt.Sprintf(" --transform 's,^,%s/,'", input.VolumeMountPrefix)
}
@@ -2851,7 +2904,7 @@ func (s *sPodGuestInstance) createSnapshot(params *SDiskSnapshot, hostPath strin
snapshotPath := s.getSnapshotPath(d, params.SnapshotId)
// tar hostPath to snapshotPath
input := params.BackupDiskConfig.BackupAsTar
if err := s.tarHostDir(hostPath, snapshotPath, input.IncludeFiles, input.ExcludeFiles, input.IgnoreNotExistFile); err != nil {
if err := s.tarHostDir(hostPath, snapshotPath, input.IncludeFiles, input.ExcludeFiles, input.IgnoreNotExistFile, input.IncludePatterns); err != nil {
return "", errors.Wrapf(err, "tar host dir %s to %s", hostPath, snapshotPath)
}
return snapshotPath, nil
@@ -2859,7 +2912,8 @@ func (s *sPodGuestInstance) createSnapshot(params *SDiskSnapshot, hostPath strin
func (s *sPodGuestInstance) tarHostDir(srcDir, targetPath string,
includeFiles, excludeFiles []string,
ignoreNotExistFile bool) error {
ignoreNotExistFile bool,
includePatterns []string) error {
baseCmd := "tar"
filterNotExistFiles := func(files []string) []string {
result := []string{}
@@ -2876,6 +2930,44 @@ func (s *sPodGuestInstance) tarHostDir(srcDir, targetPath string,
includeFiles = filterNotExistFiles(includeFiles)
excludeFiles = filterNotExistFiles(excludeFiles)
}
// 如果有 includePatterns,使用 find -name 找出匹配的路径,添加到 includeFiles 中
if len(includePatterns) > 0 {
findPatterns := []string{}
for _, pattern := range includePatterns {
// 转义特殊字符,但保留 glob 通配符
escapedPattern := strings.ReplaceAll(pattern, "'", "'\"'\"'")
findPatterns = append(findPatterns, fmt.Sprintf("-name '%s'", escapedPattern))
}
// 构建 find 命令来查找匹配的路径
findCmd := fmt.Sprintf("find . \\( %s \\)", strings.Join(findPatterns, " -o "))
cmd := fmt.Sprintf("cd '%s' && %s", srcDir, findCmd)
log.Infof("[%s] find cmd: %s", s.GetName(), cmd)
out, err := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", cmd).Output()
if err != nil {
return errors.Wrapf(err, "find matching paths: %s", out)
}
// 解析 find 的输出,将匹配的路径添加到 includeFiles 中
// find 输出的路径格式为 ./path,需要去掉开头的 ./
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// 去掉开头的 ./
if strings.HasPrefix(line, "./") {
line = line[2:]
}
// 添加到 includeFiles 中
includeFiles = append(includeFiles, line)
}
}
// 没有 includePatterns 时,使用原来的逻辑
// 添加 --exclude 选项
for _, exclude := range excludeFiles {
baseCmd = fmt.Sprintf("%s --exclude='%s'", baseCmd, exclude)
}
+4
View File
@@ -61,6 +61,7 @@ type DiskBackupCreateOptions struct {
options.BaseCreateOptions
AsTarContainerId string `help:"container id of tar process"`
AsTarIncludeFile []string `help:"include file path of tar process"`
AsTarIncludePattern []string `help:"include pattern of tar process"`
AsTarExcludeFile []string `help:"exclude file path of tar process"`
AsTarIgnoreNotExistFile bool `help:"ignore not exist file when using tar"`
@@ -82,6 +83,9 @@ func (opts *DiskBackupCreateOptions) Params() (jsonutils.JSONObject, error) {
if len(opts.AsTarIncludeFile) > 0 {
input.BackupAsTar.IncludeFiles = opts.AsTarIncludeFile
}
if len(opts.AsTarIncludePattern) > 0 {
input.BackupAsTar.IncludePatterns = opts.AsTarIncludePattern
}
if len(opts.AsTarExcludeFile) > 0 {
input.BackupAsTar.ExcludeFiles = opts.AsTarExcludeFile
}