| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- // Copyright 2019 Yunion
- //
- // 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 storageman
- import (
- "context"
- "fmt"
- "io/ioutil"
- "os"
- "path"
- "strings"
- "time"
- "yunion.io/x/jsonutils"
- "yunion.io/x/log"
- "yunion.io/x/pkg/errors"
- "yunion.io/x/pkg/util/qemuimgfmt"
- api "yunion.io/x/onecloud/pkg/apis/compute"
- "yunion.io/x/onecloud/pkg/cloudcommon/consts"
- "yunion.io/x/onecloud/pkg/cloudcommon/db"
- container_storage "yunion.io/x/onecloud/pkg/hostman/container/storage"
- "yunion.io/x/onecloud/pkg/hostman/options"
- "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage"
- _ "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage/nfs"
- _ "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage/object"
- "yunion.io/x/onecloud/pkg/mcclient/auth"
- identity_modules "yunion.io/x/onecloud/pkg/mcclient/modules/identity"
- "yunion.io/x/onecloud/pkg/util/fileutils2"
- "yunion.io/x/onecloud/pkg/util/procutils"
- "yunion.io/x/onecloud/pkg/util/qemuimg"
- )
- func EnsureBackupDir() (string, error) {
- backupTmpDir := options.HostOptions.LocalBackupTempPath
- if !fileutils2.Exists(backupTmpDir) {
- output, err := procutils.NewCommand("mkdir", "-p", backupTmpDir).Output()
- if err != nil {
- log.Errorf("mkdir %s failed: %s", backupTmpDir, output)
- return "", errors.Wrapf(err, "mkdir %s failed: %s", backupTmpDir, output)
- }
- }
- tmpFileDir, err := ioutil.TempDir(backupTmpDir, "backuptmp*")
- if err != nil {
- return "", errors.Wrap(err, "ioutil.TempDir")
- }
- return tmpFileDir, nil
- }
- func CleanupDirOrFile(path string) {
- log.Debugf("cleanup backup %s", path)
- if output, err := procutils.NewCommand("rm", "-rf", path).Output(); err != nil {
- log.Errorf("unable to rm %s: %s", path, output)
- }
- }
- func isTarSnapshot(path string) bool {
- return strings.HasSuffix(path, ".tar")
- }
- func doBackupDisk(ctx context.Context, snapshotPath string, diskBackup *SDiskBackup) (int, error) {
- backupTmpDir, err := EnsureBackupDir()
- if err != nil {
- return 0, errors.Wrap(err, "EnsureBackupDir")
- }
- defer CleanupDirOrFile(backupTmpDir)
- backupPath := path.Join(backupTmpDir, diskBackup.BackupId)
- var newImageSizeMb int
- if isTarSnapshot(snapshotPath) {
- backupPath = snapshotPath
- fileSize := fileutils2.FileSize(backupPath)
- if fileSize <= 0 {
- return 0, errors.Errorf("get snapshot path %s size failed", snapshotPath)
- }
- newImageSizeMb = int(fileSize / 1024 / 1024)
- } else {
- img, err := qemuimg.NewQemuImage(snapshotPath)
- if err != nil {
- return 0, errors.Wrap(err, "NewQemuImage snapshot")
- }
- encKey := ""
- if len(diskBackup.EncryptKeyId) > 0 {
- session := auth.GetSession(ctx, diskBackup.UserCred, consts.GetRegion())
- secKey, err := identity_modules.Credentials.GetEncryptKey(session, diskBackup.EncryptKeyId)
- if err != nil {
- return 0, errors.Wrap(err, "GetEncryptKey")
- }
- encKey = secKey.Key
- }
- if len(encKey) > 0 {
- img.SetPassword(encKey)
- }
- newImage, err := img.Clone(backupPath, qemuimgfmt.QCOW2, true)
- if err != nil {
- return 0, errors.Wrap(err, "unable to backup snapshot")
- }
- newImageSizeMb = newImage.GetActualSizeMB()
- }
- backupStorage, err := backupstorage.GetBackupStorage(diskBackup.BackupStorageId, diskBackup.BackupStorageAccessInfo)
- if err != nil {
- return 0, errors.Wrap(err, "GetBackupStorage")
- }
- err = backupstorage.SaveBackupFromFile(ctx, backupPath, diskBackup.BackupId, backupStorage)
- if err != nil {
- return 0, errors.Wrap(err, "SaveBackupFrom")
- }
- return newImageSizeMb, nil
- }
- type IDiskCreator interface {
- CreateRawDisk(ctx context.Context, disk IDisk, input *SDiskCreateByDiskinfo) (jsonutils.JSONObject, error)
- }
- func doRestoreDisk(ctx context.Context, dc IDiskCreator, input *SDiskCreateByDiskinfo, disk IDisk, destImgPath string) error {
- diskInfo := input.DiskInfo
- format := diskInfo.Format
- backupTmpDir, err := EnsureBackupDir()
- if err != nil {
- return errors.Wrap(err, "EnsureBackupDir")
- }
- defer CleanupDirOrFile(backupTmpDir)
- backupStorage, err := backupstorage.GetBackupStorage(diskInfo.Backup.BackupStorageId, diskInfo.Backup.BackupStorageAccessInfo)
- if err != nil {
- return errors.Wrap(err, "GetBackupStorage")
- }
- backupPath := path.Join(backupTmpDir, diskInfo.Backup.BackupId)
- err = backupStorage.RestoreBackupTo(ctx, backupPath, diskInfo.Backup.BackupId)
- if err != nil {
- return errors.Wrapf(err, "Restore backup %s to %s", diskInfo.Backup.BackupId, backupPath)
- }
- backupInput := diskInfo.Backup
- if backupInput.BackupAsTar != nil {
- return doRestoreTarDisk(ctx, dc, disk, input, destImgPath, backupPath)
- } else {
- return doRestoreQCOW2Disk(ctx, diskInfo, destImgPath, format, backupPath)
- }
- }
- func doRestoreQCOW2Disk(ctx context.Context, diskInfo api.DiskAllocateInput, destImgPath string, format string, backupPath string) error {
- img, err := qemuimg.NewQemuImage(backupPath)
- if err != nil {
- return errors.Wrap(err, "NewQemuImage")
- }
- if diskInfo.Encryption {
- img.SetPassword(diskInfo.EncryptInfo.Key)
- }
- if len(format) == 0 {
- format = qemuimgfmt.QCOW2.String()
- }
- _, err = img.Clone(destImgPath, qemuimgfmt.String2ImageFormat(format), false)
- if err != nil {
- return errors.Wrapf(err, "Clone %s", destImgPath)
- }
- return nil
- }
- func doRestoreTarDisk(ctx context.Context, dc IDiskCreator, disk IDisk, input *SDiskCreateByDiskinfo, destImgPath string, backupPath string) error {
- if err := input.Disk.OnRebuildRoot(ctx, input.DiskInfo); err != nil {
- return errors.Wrapf(err, "call OnRebuildRoot when restore tar disk")
- }
- diskInfo := input.DiskInfo
- backupInput := diskInfo.Backup
- if backupInput.BackupAsTar == nil {
- return errors.Error("backup.backup_as_tar input is empty")
- }
- if backupInput.DiskConfig == nil {
- return errors.Error("backup.disk_config input is empty")
- }
- _, err := dc.CreateRawDisk(ctx, disk, input)
- if err != nil {
- return errors.Wrapf(err, "CreateRawDisk by input: %s", jsonutils.Marshal(input))
- }
- drv, err := disk.GetContainerStorageDriver()
- if err != nil {
- return errors.Wrap(err, "get disk storage driver")
- }
- devPath, isConnected, err := drv.CheckConnect(destImgPath)
- if err != nil {
- return errors.Wrapf(err, "CheckConnect %s", disk.GetPath())
- }
- if !isConnected {
- devPath, err = drv.ConnectDisk(disk.GetPath())
- if err != nil {
- return errors.Wrapf(err, "ConnectDisk %s", disk.GetPath())
- }
- }
- backupMntDir, err := EnsureBackupDir()
- if err != nil {
- return errors.Wrap(err, "EnsureBackupDir")
- }
- defer CleanupDirOrFile(backupMntDir)
- if err := container_storage.Mount(devPath, backupMntDir, diskInfo.FsFormat); err != nil {
- return errors.Wrapf(err, "mount %s to %s", devPath, backupMntDir)
- }
- // 验证备份文件是否存在且有效
- if !fileutils2.Exists(backupPath) {
- return errors.Errorf("backup file does not exist: %s", backupPath)
- }
- // 检查 backupPath 是否是文件而不是目录
- fi, err := os.Stat(backupPath)
- if err != nil {
- return errors.Wrapf(err, "failed to stat backup path: %s", backupPath)
- }
- if fi.IsDir() {
- return errors.Errorf("backup path is a directory, not a file: %s. Expected a tar file but got a directory", backupPath)
- }
- // 等待文件大小稳定,确保下载完成(最多等待 5 秒)
- fileSize := fileutils2.FileSize(backupPath)
- if fileSize <= 0 {
- return errors.Errorf("backup file is empty or invalid: %s (size: %d)", backupPath, fileSize)
- }
- // 检查文件大小是否稳定(等待文件不再增长)
- maxWaitTime := 5 // 最多等待 5 秒
- checkInterval := 200 // 每 200ms 检查一次
- for i := 0; i < maxWaitTime*1000/checkInterval; i++ {
- time.Sleep(time.Duration(checkInterval) * time.Millisecond)
- newSize := fileutils2.FileSize(backupPath)
- if newSize == fileSize {
- // 文件大小稳定,下载完成
- break
- }
- if newSize < fileSize {
- // 文件大小减小,可能有问题
- log.Warningf("backup file size decreased from %d to %d bytes, file may be corrupted", fileSize, newSize)
- break
- }
- fileSize = newSize
- if i == maxWaitTime*1000/checkInterval-1 {
- log.Warningf("backup file size still changing after %d seconds, proceeding with current size: %d bytes", maxWaitTime, fileSize)
- }
- }
- log.Infof("backup file %s exists, size: %d bytes", backupPath, fileSize)
- // 验证 tar 文件完整性(使用 tar -t 测试,只列出第一个文件以快速验证)
- testCmd := fmt.Sprintf("tar -tf %s 2>&1 | head -1", backupPath)
- out, err := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", testCmd).Output()
- if err != nil {
- log.Errorf("tar file integrity test failed for %s: %s, output: %s", backupPath, err, out)
- return errors.Wrapf(err, "backup file %s appears to be corrupted or incomplete (size: %d bytes). tar test failed: %s. This usually means the backup file download was interrupted or the file is corrupted. Please retry the restore operation.", backupPath, fileSize, out)
- }
- if len(strings.TrimSpace(string(out))) == 0 {
- log.Errorf("tar file appears to be empty or corrupted: %s (size: %d bytes)", backupPath, fileSize)
- return errors.Errorf("backup file %s appears to be empty or corrupted (size: %d bytes). tar archive contains no files. This usually means the backup file download was interrupted. Please retry the restore operation.", backupPath, fileSize)
- }
- log.Infof("tar file integrity check passed for %s", backupPath)
- cmd := fmt.Sprintf("tar -xf %s -C %s", backupPath, backupMntDir)
- log.Infof("start restore %s to %s, disk: %s", backupPath, backupMntDir, disk.GetId())
- if out, err := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", cmd).Output(); err != nil {
- return errors.Wrapf(err, "%s: %s (backup file size: %d bytes). If this error persists, the backup file may be corrupted or incomplete. Please check the backup storage and retry.", cmd, out, fileSize)
- }
- if err := container_storage.Unmount(backupMntDir); err != nil {
- return errors.Wrapf(err, "unmount %s", backupMntDir)
- }
- if err := drv.DisconnectDisk(disk.GetPath(), backupMntDir); err != nil {
- return errors.Wrapf(err, "DisconnectDisk %s %s", disk.GetPath(), backupMntDir)
- }
- return nil
- }
- const (
- PackageDiskFilename = "disk"
- PackageMetadataFilename = "metadata"
- )
- func DoInstancePackBackup(ctx context.Context, backupInfo SStoragePackInstanceBackup) (string, error) {
- backupTmpDir, err := EnsureBackupDir()
- if err != nil {
- return "", errors.Wrap(err, "EnsureBackupDir")
- }
- defer CleanupDirOrFile(backupTmpDir)
- backupStorage, err := backupstorage.GetBackupStorage(backupInfo.BackupStorageId, backupInfo.BackupStorageAccessInfo)
- if err != nil {
- return "", errors.Wrap(err, "GetBackupStorage")
- }
- packagePath := path.Join(backupTmpDir, backupInfo.PackageName)
- {
- // prepare package Path
- output, err := procutils.NewCommand("mkdir", "-p", packagePath).Output()
- if err != nil {
- log.Errorf("mkdir %s failed: %s", packagePath, output)
- return "", errors.Wrapf(err, "mkdir %s failed: %s", packagePath, output)
- }
- }
- {
- // download disk files
- for i, backupId := range backupInfo.BackupIds {
- packageDiskPath := path.Join(packagePath, fmt.Sprintf("%s_%d", PackageDiskFilename, i))
- err := backupStorage.RestoreBackupTo(ctx, packageDiskPath, backupId)
- if err != nil {
- return "", errors.Wrapf(err, "RestoreBackupTo %s %s", backupId, packageDiskPath)
- }
- }
- }
- {
- // save snapshot metadata
- packageMetadataPath := path.Join(packagePath, PackageMetadataFilename)
- err = os.WriteFile(packageMetadataPath, []byte(jsonutils.Marshal(backupInfo.Metadata).PrettyString()), 0644)
- if err != nil {
- return "", errors.Wrapf(err, "unable to write to %s", packageMetadataPath)
- }
- }
- tmpPkgFilename := path.Join(backupTmpDir, backupInfo.PackageName+".tar")
- {
- // tar
- if output, err := procutils.NewRemoteCommandAsFarAsPossible("tar", "-cf", tmpPkgFilename, "-C", backupTmpDir, backupInfo.PackageName).Output(); err != nil {
- log.Errorf("unable to 'tar -cf %s -C %s %s': %s", tmpPkgFilename, backupTmpDir, backupInfo.PackageName, output)
- return "", errors.Wrap(err, "unable to tar")
- }
- }
- var finalPackageName string
- tried := 0
- for {
- var finalPackageFileName string
- if tried == 0 {
- finalPackageFileName = fmt.Sprintf("%s.tar", backupInfo.PackageName)
- } else {
- finalPackageFileName = fmt.Sprintf("%s-%d.tar", backupInfo.PackageName, tried)
- }
- exists, _, err := backupStorage.IsBackupInstanceExists(finalPackageFileName)
- if err != nil {
- return "", errors.Wrap(err, "IsBackupInstanceExists")
- }
- if exists {
- tried++
- } else {
- err := backupstorage.SaveBackupInstanceFromFile(ctx, tmpPkgFilename, finalPackageFileName, backupStorage)
- if err != nil {
- return "", errors.Wrap(err, "SaveBackupInstanceFrom")
- }
- finalPackageName = finalPackageFileName
- break
- }
- }
- return finalPackageName, nil
- }
- func DoInstanceUnpackBackup(ctx context.Context, backupInfo SStorageUnpackInstanceBackup) ([]string, *api.InstanceBackupPackMetadata, error) {
- backupTmpDir, err := EnsureBackupDir()
- if err != nil {
- return nil, nil, errors.Wrap(err, "EnsureBackupDir")
- }
- defer CleanupDirOrFile(backupTmpDir)
- packageName := backupInfo.PackageName
- metadataOnly := false
- if backupInfo.MetadataOnly != nil && *backupInfo.MetadataOnly {
- metadataOnly = true
- }
- backupStorage, err := backupstorage.GetBackupStorage(backupInfo.BackupStorageId, backupInfo.BackupStorageAccessInfo)
- if err != nil {
- return nil, nil, errors.Wrap(err, "GetBackupStorage")
- }
- packageFilename := path.Join(backupTmpDir, packageName+".tar")
- err = backupStorage.RestoreBackupInstanceTo(ctx, packageFilename, backupInfo.PackageName)
- if err != nil {
- return nil, nil, errors.Wrap(err, "RestoreBackupInstanceTo")
- }
- // untar to temp dir
- packagePath := path.Join(backupTmpDir, packageName)
- log.Infof("unpack to %s", packagePath)
- untarArgs := []string{
- "-xf", packageFilename, "-C", backupTmpDir,
- }
- if metadataOnly {
- untarArgs = append(untarArgs, fmt.Sprintf("%s/metadata", packageName))
- } else {
- untarArgs = append(untarArgs, packageName)
- }
- if output, err := procutils.NewCommand("tar", untarArgs...).Output(); err != nil {
- log.Errorf("unable to 'tar -xf %s -C %s %s': %s", packageFilename, backupTmpDir, packageName, output)
- return nil, nil, errors.Wrap(err, "unable to untar")
- }
- // unpack metadata
- packageMetadataPath := path.Join(packagePath, PackageMetadataFilename)
- metadataBytes, err := os.ReadFile(packageMetadataPath)
- if err != nil {
- return nil, nil, errors.Wrap(err, "unable to read metadata file")
- }
- metadataJson, err := jsonutils.Parse(metadataBytes)
- if err != nil {
- return nil, nil, errors.Wrap(err, "unable to parse string to json")
- }
- metadata := &api.InstanceBackupPackMetadata{}
- err = metadataJson.Unmarshal(metadata)
- if err != nil {
- return nil, nil, errors.Wrap(err, "unmarshal backup metadata")
- }
- // copy disk files only if !metadataOnly
- backupIds := make([]string, len(metadata.DiskMetadatas))
- if !metadataOnly {
- for i := 0; i < len(metadata.DiskMetadatas); i++ {
- backupId := db.DefaultUUIDGenerator()
- backupIds[i] = backupId
- packageDiskPath := path.Join(packagePath, fmt.Sprintf("%s_%d", PackageDiskFilename, i))
- err := backupstorage.SaveBackupFromFile(ctx, packageDiskPath, backupId, backupStorage)
- if err != nil {
- return nil, nil, errors.Wrapf(err, "SaveBackupFrom %s %s", packageDiskPath, backupId)
- }
- }
- }
- return backupIds, metadata, nil
- }
|