backup.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package storageman
  15. import (
  16. "context"
  17. "fmt"
  18. "io/ioutil"
  19. "os"
  20. "path"
  21. "strings"
  22. "time"
  23. "yunion.io/x/jsonutils"
  24. "yunion.io/x/log"
  25. "yunion.io/x/pkg/errors"
  26. "yunion.io/x/pkg/util/qemuimgfmt"
  27. api "yunion.io/x/onecloud/pkg/apis/compute"
  28. "yunion.io/x/onecloud/pkg/cloudcommon/consts"
  29. "yunion.io/x/onecloud/pkg/cloudcommon/db"
  30. container_storage "yunion.io/x/onecloud/pkg/hostman/container/storage"
  31. "yunion.io/x/onecloud/pkg/hostman/options"
  32. "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage"
  33. _ "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage/nfs"
  34. _ "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage/object"
  35. "yunion.io/x/onecloud/pkg/mcclient/auth"
  36. identity_modules "yunion.io/x/onecloud/pkg/mcclient/modules/identity"
  37. "yunion.io/x/onecloud/pkg/util/fileutils2"
  38. "yunion.io/x/onecloud/pkg/util/procutils"
  39. "yunion.io/x/onecloud/pkg/util/qemuimg"
  40. )
  41. func EnsureBackupDir() (string, error) {
  42. backupTmpDir := options.HostOptions.LocalBackupTempPath
  43. if !fileutils2.Exists(backupTmpDir) {
  44. output, err := procutils.NewCommand("mkdir", "-p", backupTmpDir).Output()
  45. if err != nil {
  46. log.Errorf("mkdir %s failed: %s", backupTmpDir, output)
  47. return "", errors.Wrapf(err, "mkdir %s failed: %s", backupTmpDir, output)
  48. }
  49. }
  50. tmpFileDir, err := ioutil.TempDir(backupTmpDir, "backuptmp*")
  51. if err != nil {
  52. return "", errors.Wrap(err, "ioutil.TempDir")
  53. }
  54. return tmpFileDir, nil
  55. }
  56. func CleanupDirOrFile(path string) {
  57. log.Debugf("cleanup backup %s", path)
  58. if output, err := procutils.NewCommand("rm", "-rf", path).Output(); err != nil {
  59. log.Errorf("unable to rm %s: %s", path, output)
  60. }
  61. }
  62. func isTarSnapshot(path string) bool {
  63. return strings.HasSuffix(path, ".tar")
  64. }
  65. func doBackupDisk(ctx context.Context, snapshotPath string, diskBackup *SDiskBackup) (int, error) {
  66. backupTmpDir, err := EnsureBackupDir()
  67. if err != nil {
  68. return 0, errors.Wrap(err, "EnsureBackupDir")
  69. }
  70. defer CleanupDirOrFile(backupTmpDir)
  71. backupPath := path.Join(backupTmpDir, diskBackup.BackupId)
  72. var newImageSizeMb int
  73. if isTarSnapshot(snapshotPath) {
  74. backupPath = snapshotPath
  75. fileSize := fileutils2.FileSize(backupPath)
  76. if fileSize <= 0 {
  77. return 0, errors.Errorf("get snapshot path %s size failed", snapshotPath)
  78. }
  79. newImageSizeMb = int(fileSize / 1024 / 1024)
  80. } else {
  81. img, err := qemuimg.NewQemuImage(snapshotPath)
  82. if err != nil {
  83. return 0, errors.Wrap(err, "NewQemuImage snapshot")
  84. }
  85. encKey := ""
  86. if len(diskBackup.EncryptKeyId) > 0 {
  87. session := auth.GetSession(ctx, diskBackup.UserCred, consts.GetRegion())
  88. secKey, err := identity_modules.Credentials.GetEncryptKey(session, diskBackup.EncryptKeyId)
  89. if err != nil {
  90. return 0, errors.Wrap(err, "GetEncryptKey")
  91. }
  92. encKey = secKey.Key
  93. }
  94. if len(encKey) > 0 {
  95. img.SetPassword(encKey)
  96. }
  97. newImage, err := img.Clone(backupPath, qemuimgfmt.QCOW2, true)
  98. if err != nil {
  99. return 0, errors.Wrap(err, "unable to backup snapshot")
  100. }
  101. newImageSizeMb = newImage.GetActualSizeMB()
  102. }
  103. backupStorage, err := backupstorage.GetBackupStorage(diskBackup.BackupStorageId, diskBackup.BackupStorageAccessInfo)
  104. if err != nil {
  105. return 0, errors.Wrap(err, "GetBackupStorage")
  106. }
  107. err = backupstorage.SaveBackupFromFile(ctx, backupPath, diskBackup.BackupId, backupStorage)
  108. if err != nil {
  109. return 0, errors.Wrap(err, "SaveBackupFrom")
  110. }
  111. return newImageSizeMb, nil
  112. }
  113. type IDiskCreator interface {
  114. CreateRawDisk(ctx context.Context, disk IDisk, input *SDiskCreateByDiskinfo) (jsonutils.JSONObject, error)
  115. }
  116. func doRestoreDisk(ctx context.Context, dc IDiskCreator, input *SDiskCreateByDiskinfo, disk IDisk, destImgPath string) error {
  117. diskInfo := input.DiskInfo
  118. format := diskInfo.Format
  119. backupTmpDir, err := EnsureBackupDir()
  120. if err != nil {
  121. return errors.Wrap(err, "EnsureBackupDir")
  122. }
  123. defer CleanupDirOrFile(backupTmpDir)
  124. backupStorage, err := backupstorage.GetBackupStorage(diskInfo.Backup.BackupStorageId, diskInfo.Backup.BackupStorageAccessInfo)
  125. if err != nil {
  126. return errors.Wrap(err, "GetBackupStorage")
  127. }
  128. backupPath := path.Join(backupTmpDir, diskInfo.Backup.BackupId)
  129. err = backupStorage.RestoreBackupTo(ctx, backupPath, diskInfo.Backup.BackupId)
  130. if err != nil {
  131. return errors.Wrapf(err, "Restore backup %s to %s", diskInfo.Backup.BackupId, backupPath)
  132. }
  133. backupInput := diskInfo.Backup
  134. if backupInput.BackupAsTar != nil {
  135. return doRestoreTarDisk(ctx, dc, disk, input, destImgPath, backupPath)
  136. } else {
  137. return doRestoreQCOW2Disk(ctx, diskInfo, destImgPath, format, backupPath)
  138. }
  139. }
  140. func doRestoreQCOW2Disk(ctx context.Context, diskInfo api.DiskAllocateInput, destImgPath string, format string, backupPath string) error {
  141. img, err := qemuimg.NewQemuImage(backupPath)
  142. if err != nil {
  143. return errors.Wrap(err, "NewQemuImage")
  144. }
  145. if diskInfo.Encryption {
  146. img.SetPassword(diskInfo.EncryptInfo.Key)
  147. }
  148. if len(format) == 0 {
  149. format = qemuimgfmt.QCOW2.String()
  150. }
  151. _, err = img.Clone(destImgPath, qemuimgfmt.String2ImageFormat(format), false)
  152. if err != nil {
  153. return errors.Wrapf(err, "Clone %s", destImgPath)
  154. }
  155. return nil
  156. }
  157. func doRestoreTarDisk(ctx context.Context, dc IDiskCreator, disk IDisk, input *SDiskCreateByDiskinfo, destImgPath string, backupPath string) error {
  158. if err := input.Disk.OnRebuildRoot(ctx, input.DiskInfo); err != nil {
  159. return errors.Wrapf(err, "call OnRebuildRoot when restore tar disk")
  160. }
  161. diskInfo := input.DiskInfo
  162. backupInput := diskInfo.Backup
  163. if backupInput.BackupAsTar == nil {
  164. return errors.Error("backup.backup_as_tar input is empty")
  165. }
  166. if backupInput.DiskConfig == nil {
  167. return errors.Error("backup.disk_config input is empty")
  168. }
  169. _, err := dc.CreateRawDisk(ctx, disk, input)
  170. if err != nil {
  171. return errors.Wrapf(err, "CreateRawDisk by input: %s", jsonutils.Marshal(input))
  172. }
  173. drv, err := disk.GetContainerStorageDriver()
  174. if err != nil {
  175. return errors.Wrap(err, "get disk storage driver")
  176. }
  177. devPath, isConnected, err := drv.CheckConnect(destImgPath)
  178. if err != nil {
  179. return errors.Wrapf(err, "CheckConnect %s", disk.GetPath())
  180. }
  181. if !isConnected {
  182. devPath, err = drv.ConnectDisk(disk.GetPath())
  183. if err != nil {
  184. return errors.Wrapf(err, "ConnectDisk %s", disk.GetPath())
  185. }
  186. }
  187. backupMntDir, err := EnsureBackupDir()
  188. if err != nil {
  189. return errors.Wrap(err, "EnsureBackupDir")
  190. }
  191. defer CleanupDirOrFile(backupMntDir)
  192. if err := container_storage.Mount(devPath, backupMntDir, diskInfo.FsFormat); err != nil {
  193. return errors.Wrapf(err, "mount %s to %s", devPath, backupMntDir)
  194. }
  195. // 验证备份文件是否存在且有效
  196. if !fileutils2.Exists(backupPath) {
  197. return errors.Errorf("backup file does not exist: %s", backupPath)
  198. }
  199. // 检查 backupPath 是否是文件而不是目录
  200. fi, err := os.Stat(backupPath)
  201. if err != nil {
  202. return errors.Wrapf(err, "failed to stat backup path: %s", backupPath)
  203. }
  204. if fi.IsDir() {
  205. return errors.Errorf("backup path is a directory, not a file: %s. Expected a tar file but got a directory", backupPath)
  206. }
  207. // 等待文件大小稳定,确保下载完成(最多等待 5 秒)
  208. fileSize := fileutils2.FileSize(backupPath)
  209. if fileSize <= 0 {
  210. return errors.Errorf("backup file is empty or invalid: %s (size: %d)", backupPath, fileSize)
  211. }
  212. // 检查文件大小是否稳定(等待文件不再增长)
  213. maxWaitTime := 5 // 最多等待 5 秒
  214. checkInterval := 200 // 每 200ms 检查一次
  215. for i := 0; i < maxWaitTime*1000/checkInterval; i++ {
  216. time.Sleep(time.Duration(checkInterval) * time.Millisecond)
  217. newSize := fileutils2.FileSize(backupPath)
  218. if newSize == fileSize {
  219. // 文件大小稳定,下载完成
  220. break
  221. }
  222. if newSize < fileSize {
  223. // 文件大小减小,可能有问题
  224. log.Warningf("backup file size decreased from %d to %d bytes, file may be corrupted", fileSize, newSize)
  225. break
  226. }
  227. fileSize = newSize
  228. if i == maxWaitTime*1000/checkInterval-1 {
  229. log.Warningf("backup file size still changing after %d seconds, proceeding with current size: %d bytes", maxWaitTime, fileSize)
  230. }
  231. }
  232. log.Infof("backup file %s exists, size: %d bytes", backupPath, fileSize)
  233. // 验证 tar 文件完整性(使用 tar -t 测试,只列出第一个文件以快速验证)
  234. testCmd := fmt.Sprintf("tar -tf %s 2>&1 | head -1", backupPath)
  235. out, err := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", testCmd).Output()
  236. if err != nil {
  237. log.Errorf("tar file integrity test failed for %s: %s, output: %s", backupPath, err, out)
  238. 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)
  239. }
  240. if len(strings.TrimSpace(string(out))) == 0 {
  241. log.Errorf("tar file appears to be empty or corrupted: %s (size: %d bytes)", backupPath, fileSize)
  242. 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)
  243. }
  244. log.Infof("tar file integrity check passed for %s", backupPath)
  245. cmd := fmt.Sprintf("tar -xf %s -C %s", backupPath, backupMntDir)
  246. log.Infof("start restore %s to %s, disk: %s", backupPath, backupMntDir, disk.GetId())
  247. if out, err := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", cmd).Output(); err != nil {
  248. 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)
  249. }
  250. if err := container_storage.Unmount(backupMntDir); err != nil {
  251. return errors.Wrapf(err, "unmount %s", backupMntDir)
  252. }
  253. if err := drv.DisconnectDisk(disk.GetPath(), backupMntDir); err != nil {
  254. return errors.Wrapf(err, "DisconnectDisk %s %s", disk.GetPath(), backupMntDir)
  255. }
  256. return nil
  257. }
  258. const (
  259. PackageDiskFilename = "disk"
  260. PackageMetadataFilename = "metadata"
  261. )
  262. func DoInstancePackBackup(ctx context.Context, backupInfo SStoragePackInstanceBackup) (string, error) {
  263. backupTmpDir, err := EnsureBackupDir()
  264. if err != nil {
  265. return "", errors.Wrap(err, "EnsureBackupDir")
  266. }
  267. defer CleanupDirOrFile(backupTmpDir)
  268. backupStorage, err := backupstorage.GetBackupStorage(backupInfo.BackupStorageId, backupInfo.BackupStorageAccessInfo)
  269. if err != nil {
  270. return "", errors.Wrap(err, "GetBackupStorage")
  271. }
  272. packagePath := path.Join(backupTmpDir, backupInfo.PackageName)
  273. {
  274. // prepare package Path
  275. output, err := procutils.NewCommand("mkdir", "-p", packagePath).Output()
  276. if err != nil {
  277. log.Errorf("mkdir %s failed: %s", packagePath, output)
  278. return "", errors.Wrapf(err, "mkdir %s failed: %s", packagePath, output)
  279. }
  280. }
  281. {
  282. // download disk files
  283. for i, backupId := range backupInfo.BackupIds {
  284. packageDiskPath := path.Join(packagePath, fmt.Sprintf("%s_%d", PackageDiskFilename, i))
  285. err := backupStorage.RestoreBackupTo(ctx, packageDiskPath, backupId)
  286. if err != nil {
  287. return "", errors.Wrapf(err, "RestoreBackupTo %s %s", backupId, packageDiskPath)
  288. }
  289. }
  290. }
  291. {
  292. // save snapshot metadata
  293. packageMetadataPath := path.Join(packagePath, PackageMetadataFilename)
  294. err = os.WriteFile(packageMetadataPath, []byte(jsonutils.Marshal(backupInfo.Metadata).PrettyString()), 0644)
  295. if err != nil {
  296. return "", errors.Wrapf(err, "unable to write to %s", packageMetadataPath)
  297. }
  298. }
  299. tmpPkgFilename := path.Join(backupTmpDir, backupInfo.PackageName+".tar")
  300. {
  301. // tar
  302. if output, err := procutils.NewRemoteCommandAsFarAsPossible("tar", "-cf", tmpPkgFilename, "-C", backupTmpDir, backupInfo.PackageName).Output(); err != nil {
  303. log.Errorf("unable to 'tar -cf %s -C %s %s': %s", tmpPkgFilename, backupTmpDir, backupInfo.PackageName, output)
  304. return "", errors.Wrap(err, "unable to tar")
  305. }
  306. }
  307. var finalPackageName string
  308. tried := 0
  309. for {
  310. var finalPackageFileName string
  311. if tried == 0 {
  312. finalPackageFileName = fmt.Sprintf("%s.tar", backupInfo.PackageName)
  313. } else {
  314. finalPackageFileName = fmt.Sprintf("%s-%d.tar", backupInfo.PackageName, tried)
  315. }
  316. exists, _, err := backupStorage.IsBackupInstanceExists(finalPackageFileName)
  317. if err != nil {
  318. return "", errors.Wrap(err, "IsBackupInstanceExists")
  319. }
  320. if exists {
  321. tried++
  322. } else {
  323. err := backupstorage.SaveBackupInstanceFromFile(ctx, tmpPkgFilename, finalPackageFileName, backupStorage)
  324. if err != nil {
  325. return "", errors.Wrap(err, "SaveBackupInstanceFrom")
  326. }
  327. finalPackageName = finalPackageFileName
  328. break
  329. }
  330. }
  331. return finalPackageName, nil
  332. }
  333. func DoInstanceUnpackBackup(ctx context.Context, backupInfo SStorageUnpackInstanceBackup) ([]string, *api.InstanceBackupPackMetadata, error) {
  334. backupTmpDir, err := EnsureBackupDir()
  335. if err != nil {
  336. return nil, nil, errors.Wrap(err, "EnsureBackupDir")
  337. }
  338. defer CleanupDirOrFile(backupTmpDir)
  339. packageName := backupInfo.PackageName
  340. metadataOnly := false
  341. if backupInfo.MetadataOnly != nil && *backupInfo.MetadataOnly {
  342. metadataOnly = true
  343. }
  344. backupStorage, err := backupstorage.GetBackupStorage(backupInfo.BackupStorageId, backupInfo.BackupStorageAccessInfo)
  345. if err != nil {
  346. return nil, nil, errors.Wrap(err, "GetBackupStorage")
  347. }
  348. packageFilename := path.Join(backupTmpDir, packageName+".tar")
  349. err = backupStorage.RestoreBackupInstanceTo(ctx, packageFilename, backupInfo.PackageName)
  350. if err != nil {
  351. return nil, nil, errors.Wrap(err, "RestoreBackupInstanceTo")
  352. }
  353. // untar to temp dir
  354. packagePath := path.Join(backupTmpDir, packageName)
  355. log.Infof("unpack to %s", packagePath)
  356. untarArgs := []string{
  357. "-xf", packageFilename, "-C", backupTmpDir,
  358. }
  359. if metadataOnly {
  360. untarArgs = append(untarArgs, fmt.Sprintf("%s/metadata", packageName))
  361. } else {
  362. untarArgs = append(untarArgs, packageName)
  363. }
  364. if output, err := procutils.NewCommand("tar", untarArgs...).Output(); err != nil {
  365. log.Errorf("unable to 'tar -xf %s -C %s %s': %s", packageFilename, backupTmpDir, packageName, output)
  366. return nil, nil, errors.Wrap(err, "unable to untar")
  367. }
  368. // unpack metadata
  369. packageMetadataPath := path.Join(packagePath, PackageMetadataFilename)
  370. metadataBytes, err := os.ReadFile(packageMetadataPath)
  371. if err != nil {
  372. return nil, nil, errors.Wrap(err, "unable to read metadata file")
  373. }
  374. metadataJson, err := jsonutils.Parse(metadataBytes)
  375. if err != nil {
  376. return nil, nil, errors.Wrap(err, "unable to parse string to json")
  377. }
  378. metadata := &api.InstanceBackupPackMetadata{}
  379. err = metadataJson.Unmarshal(metadata)
  380. if err != nil {
  381. return nil, nil, errors.Wrap(err, "unmarshal backup metadata")
  382. }
  383. // copy disk files only if !metadataOnly
  384. backupIds := make([]string, len(metadata.DiskMetadatas))
  385. if !metadataOnly {
  386. for i := 0; i < len(metadata.DiskMetadatas); i++ {
  387. backupId := db.DefaultUUIDGenerator()
  388. backupIds[i] = backupId
  389. packageDiskPath := path.Join(packagePath, fmt.Sprintf("%s_%d", PackageDiskFilename, i))
  390. err := backupstorage.SaveBackupFromFile(ctx, packageDiskPath, backupId, backupStorage)
  391. if err != nil {
  392. return nil, nil, errors.Wrapf(err, "SaveBackupFrom %s %s", packageDiskPath, backupId)
  393. }
  394. }
  395. }
  396. return backupIds, metadata, nil
  397. }