diff --git a/pkg/apis/compute/backup.go b/pkg/apis/compute/backup.go index 287377fe0d..19f2822ac7 100644 --- a/pkg/apis/compute/backup.go +++ b/pkg/apis/compute/backup.go @@ -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"` diff --git a/pkg/apis/compute/container.go b/pkg/apis/compute/container.go index a15eb24635..eff41ce6cb 100644 --- a/pkg/apis/compute/container.go +++ b/pkg/apis/compute/container.go @@ -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 { diff --git a/pkg/apis/container.go b/pkg/apis/container.go index 03272a73a3..9355a8ad0b 100644 --- a/pkg/apis/container.go +++ b/pkg/apis/container.go @@ -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 { diff --git a/pkg/apis/host/container.go b/pkg/apis/host/container.go index f6bdd4b03b..840cb15796 100644 --- a/pkg/apis/host/container.go +++ b/pkg/apis/host/container.go @@ -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 { diff --git a/pkg/compute/container_drivers/volume_mount/disk_pov_image.go b/pkg/compute/container_drivers/volume_mount/disk_pov_image.go index cf9d14f9c0..46dcec9f86 100644 --- a/pkg/compute/container_drivers/volume_mount/disk_pov_image.go +++ b/pkg/compute/container_drivers/volume_mount/disk_pov_image.go @@ -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 } diff --git a/pkg/compute/guestdrivers/base.go b/pkg/compute/guestdrivers/base.go index 8474e1b32d..084815ef55 100644 --- a/pkg/compute/guestdrivers/base.go +++ b/pkg/compute/guestdrivers/base.go @@ -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) +} diff --git a/pkg/compute/guestdrivers/pod.go b/pkg/compute/guestdrivers/pod.go index 3781d4acc5..56057279c2 100644 --- a/pkg/compute/guestdrivers/pod.go +++ b/pkg/compute/guestdrivers/pod.go @@ -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 { diff --git a/pkg/compute/models/containers.go b/pkg/compute/models/containers.go index bd32e826f0..4f0cf14338 100644 --- a/pkg/compute/models/containers.go +++ b/pkg/compute/models/containers.go @@ -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, "") diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index 6794078114..6994312170 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -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, "") } // 冻结虚拟机 diff --git a/pkg/compute/models/guestdrivers.go b/pkg/compute/models/guestdrivers.go index d5fbc76935..8d07df160d 100644 --- a/pkg/compute/models/guestdrivers.go +++ b/pkg/compute/models/guestdrivers.go @@ -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 diff --git a/pkg/hostman/container/storage/storage.go b/pkg/hostman/container/storage/storage.go index bbad372b3e..0e973f16d4 100644 --- a/pkg/hostman/container/storage/storage.go +++ b/pkg/hostman/container/storage/storage.go @@ -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 { diff --git a/pkg/hostman/container/volume_mount/disk/disk.go b/pkg/hostman/container/volume_mount/disk/disk.go index 50083f2465..3bbe2c228b 100644 --- a/pkg/hostman/container/volume_mount/disk/disk.go +++ b/pkg/hostman/container/volume_mount/disk/disk.go @@ -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 } diff --git a/pkg/hostman/container/volume_mount/disk/post_overlay.go b/pkg/hostman/container/volume_mount/disk/post_overlay.go index b07e2f7aa6..6fe6fe82b5 100644 --- a/pkg/hostman/container/volume_mount/disk/post_overlay.go +++ b/pkg/hostman/container/volume_mount/disk/post_overlay.go @@ -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) diff --git a/pkg/hostman/container/volume_mount/disk/post_overlay_image.go b/pkg/hostman/container/volume_mount/disk/post_overlay_image.go index f3a1d425d8..8a7d16953b 100644 --- a/pkg/hostman/container/volume_mount/disk/post_overlay_image.go +++ b/pkg/hostman/container/volume_mount/disk/post_overlay_image.go @@ -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) diff --git a/pkg/hostman/container/volume_mount/interface.go b/pkg/hostman/container/volume_mount/interface.go index 76e95fcbcc..42ff3df6bb 100644 --- a/pkg/hostman/container/volume_mount/interface.go +++ b/pkg/hostman/container/volume_mount/interface.go @@ -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 diff --git a/pkg/hostman/guestman/pod.go b/pkg/hostman/guestman/pod.go index 3c6756790a..6dd8a40f64 100644 --- a/pkg/hostman/guestman/pod.go +++ b/pkg/hostman/guestman/pod.go @@ -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) } diff --git a/pkg/mcclient/options/compute/backup.go b/pkg/mcclient/options/compute/backup.go index 428c6e31d8..9350407b35 100644 --- a/pkg/mcclient/options/compute/backup.go +++ b/pkg/mcclient/options/compute/backup.go @@ -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 }