From 3c2b1a45d1608053e90949af13882e856e4db76b Mon Sep 17 00:00:00 2001 From: rainzm Date: Wed, 19 Jan 2022 11:20:59 +0800 Subject: [PATCH 1/6] fix(cloudcommon): splice notifyurl correctly --- pkg/cloudcommon/db/taskman/tasks.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/cloudcommon/db/taskman/tasks.go b/pkg/cloudcommon/db/taskman/tasks.go index c5064fd228..28ea4801a0 100644 --- a/pkg/cloudcommon/db/taskman/tasks.go +++ b/pkg/cloudcommon/db/taskman/tasks.go @@ -19,7 +19,6 @@ import ( "database/sql" "fmt" "net/http" - "path/filepath" "reflect" "runtime/debug" "strconv" @@ -831,7 +830,7 @@ func (task *STask) GetTaskRequestHeader() http.Header { header := mcclient.GetTokenHeaders(userCred) header.Set(mcclient.TASK_ID, task.GetTaskId()) if len(serviceUrl) > 0 { - notifyUrl := filepath.Join(serviceUrl, "tasks", task.GetTaskId()) + notifyUrl := fmt.Sprintf("%s/tasks/%s", serviceUrl, task.GetTaskId()) header.Set(mcclient.TASK_NOTIFY_URL, notifyUrl) } return header From 5c6ba0d750b46fe30503114a8a57fc3a69165c7b Mon Sep 17 00:00:00 2001 From: rainzm Date: Wed, 19 Jan 2022 11:27:33 +0800 Subject: [PATCH 2/6] feat(region, host): support disk backup 1. create backup for disk 2. create disk from backup 3. recovery disk directly 4. delete backup --- pkg/apis/compute/api.go | 7 + pkg/apis/compute/backup.go | 121 ++++++ pkg/apis/compute/consts.go | 1 + pkg/apis/compute/disk.go | 10 + pkg/compute/models/backup.go | 411 ++++++++++++++++++ pkg/compute/models/backup_storage.go | 182 ++++++++ pkg/compute/models/disks.go | 50 +++ pkg/compute/models/hosts.go | 11 + pkg/compute/models/regiondrivers.go | 6 + pkg/compute/regiondrivers/base.go | 16 + pkg/compute/regiondrivers/kvm.go | 160 +++++++ pkg/compute/regiondrivers/managedvirtual.go | 4 + pkg/compute/service/handlers.go | 5 + pkg/compute/service/service.go | 12 +- pkg/compute/tasks/disk_backup_create_task.go | 197 +++++++++ pkg/compute/tasks/disk_backup_delete_task.go | 63 +++ .../tasks/disk_backup_recovery_task.go | 97 +++++ .../tasks/disk_backup_syncstatus_task.go | 58 +++ .../guestman/guesthandlers/guesthandler.go | 2 + pkg/hostman/guestman/guesthelper.go | 7 + pkg/hostman/options/options.go | 2 + .../backupstorage/backup_storage.go | 44 ++ .../backupstorage/backup_storage_nfs.go | 222 ++++++++++ pkg/hostman/storageman/backupstorage/doc.go | 15 + pkg/hostman/storageman/disk_base.go | 11 + pkg/hostman/storageman/disk_local.go | 41 ++ pkg/hostman/storageman/disk_rbd.go | 13 + .../storageman/diskhandlers/diskhandler.go | 69 +++ pkg/hostman/storageman/storage_base.go | 48 ++ pkg/hostman/storageman/storage_local.go | 56 +++ pkg/hostman/storageman/storage_rbd.go | 59 +++ .../storagehandler/storagehandler.go | 89 ++++ pkg/hostman/storageman/storagehelper.go | 13 + pkg/mcclient/options/disks.go | 3 + pkg/util/logclient/consts.go | 2 + pkg/util/qemuimg/init.go | 4 + pkg/util/qemuimg/qemuimg.go | 62 ++- 37 files changed, 2153 insertions(+), 20 deletions(-) create mode 100644 pkg/apis/compute/backup.go create mode 100644 pkg/compute/models/backup.go create mode 100644 pkg/compute/models/backup_storage.go create mode 100644 pkg/compute/tasks/disk_backup_create_task.go create mode 100644 pkg/compute/tasks/disk_backup_delete_task.go create mode 100644 pkg/compute/tasks/disk_backup_recovery_task.go create mode 100644 pkg/compute/tasks/disk_backup_syncstatus_task.go create mode 100644 pkg/hostman/storageman/backupstorage/backup_storage.go create mode 100644 pkg/hostman/storageman/backupstorage/backup_storage_nfs.go create mode 100644 pkg/hostman/storageman/backupstorage/doc.go diff --git a/pkg/apis/compute/api.go b/pkg/apis/compute/api.go index f128c89f51..4cd656418d 100644 --- a/pkg/apis/compute/api.go +++ b/pkg/apis/compute/api.go @@ -107,6 +107,8 @@ type DiskConfig struct { // requried: false SnapshotId string `json:"snapshot_id"` + BackupId string `json:"backup_id"` + // 磁盘类型 // enum: sys, data, swap DiskType string `json:"disk_type"` @@ -495,6 +497,11 @@ type ServerCreateInput struct { // required: false InstanceSnapshotId string `json:"instance_snapshot_id,omitempty"` + // 使用主机备份创建虚拟机, 主机快照不会重置密码及秘钥信息 + // 使用主机备份创建的虚拟机将沿用之前的密码秘钥及安全组信息 + // required: false + InstanceBackupId string `json:"instance_backup_id,omitempty"` + // 安全组Id, 此参数会和secgroups参数合并 SecgroupId string `json:"secgrp_id"` // 安全组Id列表 diff --git a/pkg/apis/compute/backup.go b/pkg/apis/compute/backup.go new file mode 100644 index 0000000000..af815f02b6 --- /dev/null +++ b/pkg/apis/compute/backup.go @@ -0,0 +1,121 @@ +// 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 compute + +import "yunion.io/x/onecloud/pkg/apis" + +const ( + BACKUPSTORAGE_TYPE_NFS = "nfs" + BACKUPSTORAGE_STATUS_ONLINE = "online" + + BACKUP_STATUS_CREATING = "creating" + BACKUP_STATUS_CREATE_FAILED = "create_failed" + BACKUP_STATUS_SNAPSHOT = "snapshot" + BACKUP_STATUS_SNAPSHOT_FAILED = "snapshot_failed" + BACKUP_STATUS_SAVING = "saving" + BACKUP_STATUS_SAVE_FAILED = "save_failed" + BACKUP_STATUS_CLEANUP_SNAPSHOT = "clean_snapshot" + BACKUP_STATUS_CLEANUP_SNAPSHOT_FAILED = "clean_snapshot_failed" + BACKUP_STATUS_DELETING = "deleting" + BACKUP_STATUS_DELETE_FAILED = "delete_failed" + BACKUP_STATUS_READY = "ready" + BACKUP_STATUS_RECOVERY = "recovery" + BACKUP_STATUS_RECOVERY_FAILED = "recovery_failed" + BACKUP_STATUS_UNKNOWN = "unknown" + + BACKUP_EXIST = "exist" + BACKUP_NOT_EXIST = "not_exist" +) + +type BackupStorageCreateInput struct { + apis.EnabledStatusInfrasResourceBaseCreateInput + + // description: storage type + // enum: nfs + StorageType string `json:"storage_type"` + + // description: host of nfs, storage_type 为 nfs 时, 此参数必传 + // example: 192.168.222.2 + NfsHost string `json:"nfs_host"` + + // description: shared dir of nfs, storage_type 为 nfs 时, 此参数必传 + // example: /nfs_root/ + NfsSharedDir string `json:"nfs_shared_dir"` + + // description: Capacity size in MB + CapacityMb int `json:"capacity_mb"` +} + +type BackupStorageAccessInfo struct { + AccessUrl string +} + +type BackupStorageDetails struct { + apis.EnabledStatusInfrasResourceBaseDetails + + NfsHost string + NfsSharedDir string +} + +type BackupStorageListInput struct { + apis.EnabledStatusInfrasResourceBaseListInput +} + +type DiskBackupListInput struct { + apis.VirtualResourceListInput + ManagedResourceListInput + RegionalFilterListInput + apis.MultiArchResourceBaseListInput + // description: disk id + DiskId string `json:"disk_id"` + // description: backup storage id + BackupStorageId string `json:"backup_storage_id"` + // description: 是否为主机备份的一部分 + IsInstanceBackup *bool `json:"is_instance_backup"` +} + +type DiskBackupDetails struct { + apis.VirtualResourceDetails + ManagedResourceInfo + CloudregionResourceInfo + + // description: disk name + DiskName string `json:"disk_name"` + // description: backup storage name + BackupStorageName string `json:"backup_storage_name"` + // description: 是否是子备份 + IsSubBackup bool `json:"is_sub_backup"` +} + +type DiskBackupCreateInput struct { + apis.VirtualResourceCreateInput + + // description: disk id + DiskId string `json:"disk_id"` + // description: backup storage id + BackupStorageId string `json:"back_storage_id"` + // swagger: ignore + CloudregionId string `json:"cloudregion_id"` + // swagger:ignore + ManagerId string `json:"manager_id"` +} + +type DiskBackupRecoveryInput struct { + // description: name of disk + Name string +} + +type DiskBackupSyncstatusInput struct { +} diff --git a/pkg/apis/compute/consts.go b/pkg/apis/compute/consts.go index 908c999501..9cb10b26a4 100644 --- a/pkg/apis/compute/consts.go +++ b/pkg/apis/compute/consts.go @@ -19,4 +19,5 @@ import "yunion.io/x/onecloud/pkg/apis" const ( SERVICE_TYPE = apis.SERVICE_TYPE_REGION SERVICE_VERSION = "v2" + SERVER_TYPE_V2 = apis.SERVICE_TYPE_REGION + "_" + SERVICE_VERSION ) diff --git a/pkg/apis/compute/disk.go b/pkg/apis/compute/disk.go index 68c87c05e6..50afa624bf 100644 --- a/pkg/apis/compute/disk.go +++ b/pkg/apis/compute/disk.go @@ -17,6 +17,7 @@ package compute import ( "time" + "yunion.io/x/jsonutils" "yunion.io/x/pkg/util/fileutils" "yunion.io/x/onecloud/pkg/apis" @@ -277,6 +278,9 @@ type DiskAllocateInput struct { BackingDiskId string SnapshotId string + BackupId string + Backup *DiskAllocateFromBackupInput + SnapshotUrl string SnapshotOutOfChain bool Protocol string @@ -287,3 +291,9 @@ type DiskAllocateInput struct { HostIp string Datastore vcenter.SVCenterAccessInfo } + +type DiskAllocateFromBackupInput struct { + BackupId string + BackupStorageId string + BackupStorageAccessInfo *jsonutils.JSONDict +} diff --git a/pkg/compute/models/backup.go b/pkg/compute/models/backup.go new file mode 100644 index 0000000000..22f715a891 --- /dev/null +++ b/pkg/compute/models/backup.go @@ -0,0 +1,411 @@ +// 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 models + +import ( + "context" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/sqlchemy" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudcommon/validators" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +type SDiskBackupManager struct { + db.SVirtualResourceBaseManager + SDiskResourceBaseManager + SManagedResourceBaseManager + SCloudregionResourceBaseManager + db.SMultiArchResourceBaseManager +} + +type SDiskBackup struct { + db.SVirtualResourceBase + + SManagedResourceBase + SCloudregionResourceBase `width:"36" charset:"ascii" nullable:"true" list:"user" create:"optional"` + db.SMultiArchResourceBase + + DiskId string `width:"36" charset:"ascii" nullable:"true" create:"required" list:"user" index:"true"` + BackupStorageId string `width:"36" charset:"ascii" nullable:"true" create:"required" list:"user" index:"true"` + StorageId string `width:"36" charset:"ascii" nullable:"true" list:"user"` + + // 备份大小 + SizeMb int `nullable:"false" list:"user" create:"optional"` + DiskSizeMb int `nullable:"false" list:"user" create:"optional"` + DiskType string `width:"32" charset:"ascii" nullable:"true" list:"user" create:"optional"` + // 操作系统类型 + OsType string `width:"32" charset:"ascii" nullable:"true" list:"user" create:"optional"` + DiskConfig jsonutils.JSONObject +} + +var DiskBackupManager *SDiskBackupManager + +func init() { + DiskBackupManager = &SDiskBackupManager{ + SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager( + SDiskBackup{}, + "diskbackups_tbl", + "diskbackup", + "diskbackups", + ), + } + DiskBackupManager.SetVirtualObject(DiskBackupManager) +} + +func (dm *SDiskBackupManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.DiskBackupListInput) (*sqlchemy.SQuery, error) { + var err error + q, err = dm.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, input.VirtualResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter") + } + q, err = dm.SManagedResourceBaseManager.ListItemFilter(ctx, q, userCred, input.ManagedResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SManagedResourceBaseManager.ListItemFilter") + } + q, err = dm.SCloudregionResourceBaseManager.ListItemFilter(ctx, q, userCred, input.RegionalFilterListInput) + if err != nil { + return nil, errors.Wrap(err, "SCloudregionResourceBaseManager.ListItemFilter") + } + q, err = dm.SMultiArchResourceBaseManager.ListItemFilter(ctx, q, userCred, input.MultiArchResourceBaseListInput) + if err != nil { + return nil, errors.Wrap(err, "SMultiArchResourceBaseManager.ListItemFilter") + } + if input.DiskId != "" { + q = q.Equals("disk_id", input.DiskId) + } + if input.BackupStorageId != "" { + q = q.Equals("backup_storage_id", input.BackupStorageId) + } + if input.IsInstanceBackup != nil { + insjsq := InstanceBackupJointManager.Query().SubQuery() + if !*input.IsInstanceBackup { + q = q.LeftJoin(insjsq, sqlchemy.Equals(q.Field("id"), insjsq.Field("disk_backup_id"))). + Filter(sqlchemy.IsNull(insjsq.Field("disk_backup_id"))) + } else { + q = q.Join(insjsq, sqlchemy.Equals(q.Field("id"), insjsq.Field("disk_backup_id"))) + } + } + return q, nil +} + +func (self *SDiskBackup) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error { + if self.Status == api.BACKUP_STATUS_DELETING { + return httperrors.NewBadRequestError("Cannot delete disk backup in status %s", self.Status) + } + is, err := InstanceBackupJointManager.IsSubBackup(self.Id) + if err != nil { + return err + } + if is { + return httperrors.NewBadRequestError("disk backup referenced by instance backup") + } + return nil +} + +func (dm *SDiskBackupManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.DiskBackupDetails { + rows := make([]api.DiskBackupDetails, len(objs)) + virtRows := dm.SVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + manRows := dm.SManagedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + regionRows := dm.SCloudregionResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + + for i := range rows { + rows[i].VirtualResourceDetails = virtRows[i] + rows[i].ManagedResourceInfo = manRows[i] + rows[i].CloudregionResourceInfo = regionRows[i] + rows[i] = objs[i].(*SDiskBackup).getMoreDetails(rows[i]) + } + return rows +} + +func (db *SDiskBackup) getMoreDetails(out api.DiskBackupDetails) api.DiskBackupDetails { + disk, _ := db.GetDisk() + if disk != nil { + out.DiskName = disk.Name + } + backupStorage, _ := db.GetBackupStorage() + if backupStorage != nil { + out.BackupStorageName = backupStorage.GetName() + } + if t, _ := InstanceBackupJointManager.IsSubBackup(db.Id); t { + out.IsSubBackup = true + } + return out +} + +func (db *SDiskBackup) GetDisk() (*SDisk, error) { + iDisk, err := DiskManager.FetchById(db.DiskId) + if err != nil { + return nil, err + } + disk := iDisk.(*SDisk) + return disk, nil +} + +func (db *SDiskBackup) GetStorage() (*SStorage, error) { + iStorage, err := StorageManager.FetchById(db.StorageId) + if err != nil { + return nil, err + } + return iStorage.(*SStorage), nil +} + +func (db *SDiskBackup) GetBackupStorage() (*SBackupStorage, error) { + ibs, err := BackupStorageManager.FetchById(db.BackupStorageId) + if err != nil { + return nil, err + } + return ibs.(*SBackupStorage), nil +} + +func (db *SDiskBackup) GetRegionDriver() IRegionDriver { + cloudRegion, _ := db.GetRegion() + if cloudRegion != nil { + return cloudRegion.GetDriver() + } + return nil +} + +func (dm *SDiskBackupManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.DiskBackupCreateInput) (api.DiskBackupCreateInput, error) { + if len(input.DiskId) == 0 { + return input, httperrors.NewMissingParameterError("disk_id") + } + if len(input.BackupStorageId) == 0 { + return input, httperrors.NewMissingParameterError("backup_storage_id") + } + // check disk + _disk, err := validators.ValidateModel(userCred, DiskManager, &input.DiskId) + if err != nil { + return input, err + } + disk := _disk.(*SDisk) + if disk.Status != api.DISK_READY { + return input, httperrors.NewInvalidStatusError("disk %s status is not %s", disk.Name, api.DISK_READY) + } + _, err = validators.ValidateModel(userCred, BackupStorageManager, &input.BackupStorageId) + if err != nil { + return input, err + } + storage, err := disk.GetStorage() + if err != nil { + return input, errors.Wrapf(err, "unable to get storage of disk %s", disk.GetId()) + } + input.ManagerId = storage.ManagerId + region, err := storage.GetRegion() + if err != nil { + return input, err + } + input.CloudregionId = region.Id + + return input, nil +} + +func (db *SDiskBackup) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + err := db.SVirtualResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data) + if err != nil { + return err + } + diskObj, err := DiskManager.FetchById(db.DiskId) + if err != nil { + return errors.Wrap(err, "DiskManager.FetchById") + } + disk := diskObj.(*SDisk) + diskConfig := jsonutils.Marshal(disk.ToDiskConfig()).(*jsonutils.JSONDict) + diskConfig.Set("name", jsonutils.NewString(disk.GetName())) + db.DiskConfig = diskConfig + db.DiskType = disk.DiskType + db.DiskSizeMb = disk.DiskSize + db.OsArch = disk.OsArch + db.StorageId = disk.StorageId + return nil +} + +func (db *SDiskBackup) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { + db.SVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data) + db.StartBackupCreateTask(ctx, userCred, nil, "") +} + +func (db *SDiskBackup) StartBackupCreateTask(ctx context.Context, userCred mcclient.TokenCredential, params *jsonutils.JSONDict, parentTaskId string) error { + task, err := taskman.TaskManager.NewTask(ctx, "DiskBackupCreateTask", db, userCred, params, parentTaskId, "", nil) + if err != nil { + return err + } + task.ScheduleRun(nil) + return nil +} + +func (manager *SDiskBackupManager) ListItemExportKeys(ctx context.Context, + q *sqlchemy.SQuery, + userCred mcclient.TokenCredential, + keys stringutils2.SSortedStrings, +) (*sqlchemy.SQuery, error) { + var err error + q, err = manager.SVirtualResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys) + if err != nil { + return nil, err + } + if keys.Contains("disk") { + q, err = manager.SDiskResourceBaseManager.ListItemExportKeys(ctx, q, userCred, stringutils2.NewSortedStrings([]string{"disk"})) + if err != nil { + return nil, errors.Wrap(err, "SDiskResourceBaseManager.ListItemExportKeys") + } + } + if keys.ContainsAny(manager.SStorageResourceBaseManager.GetExportKeys()...) { + q, err = manager.SStorageResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys) + if err != nil { + return nil, errors.Wrap(err, "SStorageResourceBaseManager.ListItemExportKeys") + } + } + + return q, nil +} + +func (manager *SDiskBackupManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { + var err error + + q, err = manager.SVirtualResourceBaseManager.QueryDistinctExtraField(q, field) + if err == nil { + return q, nil + } + + q, err = manager.SManagedResourceBaseManager.QueryDistinctExtraField(q, field) + if err == nil { + return q, nil + } + + q, err = manager.SCloudregionResourceBaseManager.QueryDistinctExtraField(q, field) + if err == nil { + return q, nil + } + + return q, httperrors.ErrNotFound +} + +func (manager *SDiskBackupManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.DiskBackupListInput) (*sqlchemy.SQuery, error) { + var err error + q, err = manager.SVirtualResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.VirtualResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.OrderByExtraFields") + } + q, err = manager.SManagedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.ManagedResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SManagedResourceBaseManager.OrderByExtraFields") + } + + q, err = manager.SCloudregionResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.RegionalFilterListInput) + if err != nil { + return nil, errors.Wrap(err, "SCloudregionResourceBaseManager.OrderByExtraFields") + } + return q, nil +} + +func (self *SDiskBackup) Delete(ctx context.Context, userCred mcclient.TokenCredential) error { + return nil +} + +func (self *SDiskBackup) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error { + return db.DeleteModel(ctx, userCred, self) +} + +func (self *SDiskBackup) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + return self.StartBackupDeleteTask(ctx, userCred, "") +} + +func (self *SDiskBackup) StartBackupDeleteTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error { + self.SetStatus(userCred, api.BACKUP_STATUS_DELETING, "") + log.Infof("start to delete diskbackup %s and set deleting", self.GetId()) + task, err := taskman.TaskManager.NewTask(ctx, "DiskBackupDeleteTask", self, userCred, nil, parentTaskId, "", nil) + if err != nil { + return err + } else { + task.ScheduleRun(nil) + } + return nil +} + +func (self *SDiskBackup) PerformRecovery(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.DiskBackupRecoveryInput) (jsonutils.JSONObject, error) { + return nil, self.StartRecoveryTask(ctx, userCred, "", input.Name) +} + +func (self *SDiskBackup) StartRecoveryTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string, diskName string) error { + self.SetStatus(userCred, api.BACKUP_STATUS_RECOVERY, "") + var params *jsonutils.JSONDict + if diskName != "" { + params = jsonutils.NewDict() + params.Set("disk_name", jsonutils.NewString(diskName)) + } + task, err := taskman.TaskManager.NewTask(ctx, "DiskBackupRecoveryTask", self, userCred, params, parentTaskId, "", nil) + if err != nil { + return err + } else { + task.ScheduleRun(nil) + } + return nil +} + +func (manager *SDiskBackupManager) CreateBackup(ctx context.Context, owner mcclient.IIdentityProvider, diskId, backupStorageId, name string) (*SDiskBackup, error) { + iDisk, err := DiskManager.FetchById(diskId) + if err != nil { + return nil, err + } + disk := iDisk.(*SDisk) + storage, _ := disk.GetStorage() + backup := &SDiskBackup{} + backup.SetModelManager(manager, backup) + backup.ProjectId = owner.GetProjectId() + backup.DomainId = owner.GetProjectDomainId() + backup.DiskId = disk.Id + diskConfig := jsonutils.Marshal(disk.ToDiskConfig()).(*jsonutils.JSONDict) + diskConfig.Set("name", jsonutils.NewString(disk.GetName())) + backup.DiskConfig = diskConfig + backup.DiskType = disk.DiskType + backup.DiskSizeMb = disk.DiskSize + backup.OsArch = disk.OsArch + backup.StorageId = disk.StorageId + backup.ManagerId = storage.ManagerId + if cloudregion, _ := storage.GetRegion(); cloudregion != nil { + backup.CloudregionId = cloudregion.GetId() + } + backup.BackupStorageId = backupStorageId + backup.Name = name + backup.Status = api.BACKUP_STATUS_CREATING + err = DiskBackupManager.TableSpec().Insert(ctx, backup) + if err != nil { + return nil, err + } + return backup, nil +} + +func (self *SDiskBackup) PerformSyncstatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.DiskBackupSyncstatusInput) (jsonutils.JSONObject, error) { + var openTask = true + count, err := taskman.TaskManager.QueryTasksOfObject(self, time.Now().Add(-3*time.Minute), &openTask).CountWithError() + if err != nil { + return nil, err + } + if count > 0 { + return nil, httperrors.NewBadRequestError("Snapshot has %d task active, can't sync status", count) + } + + return nil, StartResourceSyncStatusTask(ctx, userCred, self, "DiskBackupSyncstatusTask", "") +} diff --git a/pkg/compute/models/backup_storage.go b/pkg/compute/models/backup_storage.go new file mode 100644 index 0000000000..6a84f18e3e --- /dev/null +++ b/pkg/compute/models/backup_storage.go @@ -0,0 +1,182 @@ +// 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 models + +import ( + "context" + "reflect" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/gotypes" + "yunion.io/x/pkg/tristate" + "yunion.io/x/pkg/utils" + "yunion.io/x/sqlchemy" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +type SBackupStorageManager struct { + db.SEnabledStatusInfrasResourceBaseManager +} + +type SBackupStorage struct { + db.SEnabledStatusInfrasResourceBase + + AccessInfo *SBackupStorageAccessInfo + StorageType string `width:"32" charset:"ascii" nullable:"false" list:"user" create:"domain_required"` + CapacityMb int `nullable:"false" list:"user" update:"domain" create:"domain_required"` +} + +var BackupStorageManager *SBackupStorageManager + +func init() { + gotypes.RegisterSerializable(reflect.TypeOf(&SBackupStorageAccessInfo{}), func() gotypes.ISerializable { + return &SBackupStorageAccessInfo{} + }) + BackupStorageManager = &SBackupStorageManager{ + SEnabledStatusInfrasResourceBaseManager: db.NewEnabledStatusInfrasResourceBaseManager( + SBackupStorage{}, + "backupstorages_tbl", + "backupstorage", + "backupstorages", + ), + } + BackupStorageManager.SetVirtualObject(BackupStorageManager) +} + +type SBackupStorageAccessInfo struct { + NfsHost string `json:"nfs_host"` + NfsSharedDir string `json:"nfs_shared_dir"` +} + +func (ba *SBackupStorageAccessInfo) String() string { + return jsonutils.Marshal(ba).String() +} + +func (ba *SBackupStorageAccessInfo) IsZero() bool { + return ba == nil +} + +func (bs *SBackupStorageManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.BackupStorageCreateInput) (api.BackupStorageCreateInput, error) { + var err error + input.EnabledStatusInfrasResourceBaseCreateInput, err = bs.SEnabledStatusInfrasResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.EnabledStatusInfrasResourceBaseCreateInput) + if err != nil { + return input, err + } + if !utils.IsInStringArray(input.StorageType, []string{api.BACKUPSTORAGE_TYPE_NFS}) { + return input, httperrors.NewInputParameterError("Invalid storage type %s", input.StorageType) + } + switch input.StorageType { + case api.BACKUPSTORAGE_TYPE_NFS: + if input.NfsHost == "" { + return input, httperrors.NewInputParameterError("nfs_host is required when storage type is nfs") + } + if input.NfsSharedDir == "" { + return input, httperrors.NewInputParameterError("nfs_shared_dir is required when storage type is nfs") + } + + } + return input, nil +} + +func (bs *SBackupStorage) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + bs.SetEnabled(true) + nfsHost, _ := data.GetString("nfs_host") + nfsSharedDir, _ := data.GetString("nfs_shared_dir") + bs.Status = api.BACKUPSTORAGE_STATUS_ONLINE + bs.AccessInfo = &SBackupStorageAccessInfo{ + NfsHost: nfsHost, + NfsSharedDir: nfsSharedDir, + } + return bs.SEnabledStatusInfrasResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data) +} + +func (bs *SBackupStorage) BackupCount() (int, error) { + return 0, nil +} + +func (bs *SBackupStorage) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error { + cnt, err := bs.BackupCount() + if err != nil { + return httperrors.NewInternalServerError("BackupCount fail %s", err) + } + if cnt > 0 { + return httperrors.NewNotEmptyError("storage has backup") + } + return bs.SEnabledStatusInfrasResourceBase.ValidateDeleteCondition(ctx, nil) +} + +func (bs *SBackupStorage) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { + bs.SEnabledStatusInfrasResourceBase.PostCreate(ctx, userCred, ownerId, query, data) +} + +func (bs *SBackupStorage) PerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + if bs.Enabled.IsFalse() { + _, err := db.Update(bs, func() error { + bs.Enabled = tristate.True + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "unable to enable") + } + db.OpsLog.LogEvent(bs, db.ACT_ENABLE, "", userCred) + } + return nil, nil +} + +func (bs *SBackupStorage) PerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + if bs.Enabled.IsTrue() { + _, err := db.Update(bs, func() error { + bs.Enabled = tristate.False + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "unable to disable") + } + db.OpsLog.LogEvent(bs, db.ACT_DISABLE, "", userCred) + } + return nil, nil +} + +func (bs *SBackupStorage) getMoreDetails(ctx context.Context, out api.BackupStorageDetails) api.BackupStorageDetails { + out.NfsHost = bs.AccessInfo.NfsHost + out.NfsSharedDir = bs.AccessInfo.NfsSharedDir + return out +} + +func (bm *SBackupStorageManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.BackupStorageDetails { + rows := make([]api.BackupStorageDetails, len(objs)) + esiRows := bm.SEnabledStatusInfrasResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + for i := range rows { + rows[i].EnabledStatusInfrasResourceBaseDetails = esiRows[i] + bs := objs[i].(*SBackupStorage) + rows[i] = bs.getMoreDetails(ctx, rows[i]) + } + return rows +} + +func (bm *SBackupStorageManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.BackupStorageListInput) (*sqlchemy.SQuery, error) { + var err error + q, err = bm.SEnabledStatusInfrasResourceBaseManager.ListItemFilter(ctx, q, userCred, input.EnabledStatusInfrasResourceBaseListInput) + if err != nil { + return nil, err + } + return q, nil +} diff --git a/pkg/compute/models/disks.go b/pkg/compute/models/disks.go index 0f071b16ba..3f8da3523f 100644 --- a/pkg/compute/models/disks.go +++ b/pkg/compute/models/disks.go @@ -104,6 +104,8 @@ type SDisk struct { TemplateId string `width:"256" charset:"ascii" nullable:"true" list:"user" json:"template_id"` // 快照Id SnapshotId string `width:"256" charset:"ascii" nullable:"true" list:"user" json:"snapshot_id"` + // 备份Id + BackupId string `width:"256" charset:"ascii" nullable:"true" list:"user" json:"backup_id"` // 文件系统 FsFormat string `width:"32" charset:"ascii" nullable:"true" list:"user" json:"fs_format"` @@ -687,6 +689,23 @@ func (self *SDisk) GetManualSnapshotCount() (int, error) { Equals("created_by", api.SNAPSHOT_MANUAL).CountWithError() } +func (self *SDisk) getDiskAllocateFromBackupInput(ctx context.Context, backupId string) (*api.DiskAllocateFromBackupInput, error) { + ibackup, err := DiskBackupManager.FetchById(backupId) + if err != nil { + return nil, errors.Wrapf(err, "unable to get backup %s", backupId) + } + backup := ibackup.(*SDiskBackup) + bs, err := backup.GetBackupStorage() + if err != nil { + return nil, errors.Wrapf(err, "unable to get backupstorage of backup %s", backupId) + } + return &api.DiskAllocateFromBackupInput{ + BackupId: backupId, + BackupStorageId: bs.GetId(), + BackupStorageAccessInfo: jsonutils.Marshal(bs.AccessInfo).(*jsonutils.JSONDict), + }, nil +} + func (self *SDisk) StartAllocate(ctx context.Context, host *SHost, storage *SStorage, taskId string, userCred mcclient.TokenCredential, rebuild bool, snapshot string, task taskman.ITask) error { log.Infof("Allocating disk on host %s ...", host.GetName()) @@ -698,6 +717,13 @@ func (self *SDisk) StartAllocate(ctx context.Context, host *SHost, storage *SSto DiskSizeMb: self.DiskSize, SnapshotId: snapshot, } + if self.BackupId != "" { + allocateInput, err := self.getDiskAllocateFromBackupInput(ctx, self.BackupId) + if err != nil { + return errors.Wrap(err, "unable to getDiskAllocateFromBackupInput") + } + input.Backup = allocateInput + } if len(snapshot) > 0 { if utils.IsInStringArray(storage.StorageType, api.FIEL_STORAGE) { SnapshotManager.AddRefCount(self.SnapshotId, 1) @@ -1718,6 +1744,11 @@ func parseDiskInfo(ctx context.Context, userCred mcclient.TokenCredential, info return nil, errors.Wrap(err, "fillDiskConfigByImage") } } + if info.BackupId != "" { + if err := fillDiskConfigByBackup(ctx, userCred, info, info.BackupId); err != nil { + return nil, errors.Wrap(err, "fillDiskConfigByBackup") + } + } // XXX: do not set default disk size here, set it by each hypervisor driver // if len(diskConfig.ImageId) > 0 && diskConfig.SizeMb == 0 { // diskConfig.SizeMb = options.Options.DefaultDiskSize // MB @@ -1758,6 +1789,22 @@ func fillDiskConfigBySnapshot(userCred mcclient.TokenCredential, diskConfig *api return nil } +func fillDiskConfigByBackup(ctx context.Context, userCred mcclient.TokenCredential, diskConfig *api.DiskConfig, backupId string) error { + iBakcup, err := DiskBackupManager.FetchByIdOrName(userCred, backupId) + if err != nil { + if err == sql.ErrNoRows { + return httperrors.NewNotFoundError("Backup %s not found", backupId) + } + return err + } + backup := iBakcup.(*SDiskBackup) + if diskConfig.DiskType == "" { + diskConfig.DiskType = backup.DiskType + } + diskConfig.BackupId = backup.GetId() + return nil +} + func fillDiskConfigByImage(ctx context.Context, userCred mcclient.TokenCredential, diskConfig *api.DiskConfig, imageId string) error { if userCred == nil { @@ -1880,6 +1927,9 @@ func (self *SDisk) fetchDiskInfo(diskConfig *api.DiskConfig) { } else if len(diskConfig.SnapshotId) > 0 { self.SnapshotId = diskConfig.SnapshotId self.DiskType = diskConfig.DiskType + } else if len(diskConfig.BackupId) > 0 { + self.BackupId = diskConfig.BackupId + self.DiskType = diskConfig.DiskType } if len(diskConfig.Fs) > 0 { self.FsFormat = diskConfig.Fs diff --git a/pkg/compute/models/hosts.go b/pkg/compute/models/hosts.go index c86ba86778..bf7026abdd 100644 --- a/pkg/compute/models/hosts.go +++ b/pkg/compute/models/hosts.go @@ -1383,6 +1383,17 @@ func (self *SHost) GetMasterHostwire() *SHostwire { return &hw } +func (self *SHostManager) GetEnabledHost() (*SHost, error) { + hostq := HostManager.Query().IsTrue("enabled").Equals("host_status", api.HOST_ONLINE) + host := SHost{} + err := hostq.First(&host) + if err != nil { + return nil, err + } + host.SetModelManager(HostManager, &host) + return &host, nil +} + func (self *SHost) GetMasterWire() *SWire { wires := WireManager.Query().SubQuery() hostwires := HostwireManager.Query().SubQuery() diff --git a/pkg/compute/models/regiondrivers.go b/pkg/compute/models/regiondrivers.go index cd4119dd40..9d7d307910 100644 --- a/pkg/compute/models/regiondrivers.go +++ b/pkg/compute/models/regiondrivers.go @@ -117,6 +117,12 @@ type IRegionDriver interface { OnDiskReset(ctx context.Context, userCred mcclient.TokenCredential, disk *SDisk, snapshot *SSnapshot, data jsonutils.JSONObject) error OnSnapshotDelete(ctx context.Context, snapshot *SSnapshot, task taskman.ITask, data jsonutils.JSONObject) error + RequestSyncDiskBackupStatus(ctx context.Context, userCred mcclient.TokenCredential, backup *SDiskBackup, task taskman.ITask) error + RequestCreateBackup(ctx context.Context, backup *SDiskBackup, snapshotId string, task taskman.ITask) error + RequestDeleteBackup(ctx context.Context, backup *SDiskBackup, task taskman.ITask) error + RequestCreateInstanceBackup(ctx context.Context, guest *SGuest, ib *SInstanceBackup, task taskman.ITask, params *jsonutils.JSONDict) error + RequestDeleteInstanceBackup(ctx context.Context, ib *SInstanceBackup, task taskman.ITask) error + RequestCreateInstanceSnapshot(ctx context.Context, guest *SGuest, isp *SInstanceSnapshot, task taskman.ITask, params *jsonutils.JSONDict) error RequestDeleteInstanceSnapshot(ctx context.Context, isp *SInstanceSnapshot, task taskman.ITask) error RequestResetToInstanceSnapshot(ctx context.Context, guest *SGuest, isp *SInstanceSnapshot, task taskman.ITask, params *jsonutils.JSONDict) error diff --git a/pkg/compute/regiondrivers/base.go b/pkg/compute/regiondrivers/base.go index cb74b15f66..dd56c91c7d 100644 --- a/pkg/compute/regiondrivers/base.go +++ b/pkg/compute/regiondrivers/base.go @@ -473,6 +473,22 @@ func (self *SBaseRegionDriver) RequestCreateNetwork(ctx context.Context, userCre return errors.Wrapf(cloudprovider.ErrNotImplemented, "RequestCreateNetwork") } +func (self *SBaseRegionDriver) RequestCreateBackup(ctx context.Context, backup *models.SDiskBackup, snapshotId string, task taskman.ITask) error { + return errors.Wrapf(cloudprovider.ErrNotImplemented, "RequestCreateBackup") +} + +func (self *SBaseRegionDriver) RequestDeleteBackup(ctx context.Context, backup *models.SDiskBackup, task taskman.ITask) error { + return errors.Wrapf(cloudprovider.ErrNotImplemented, "RequestDeleteBackup") +} + +func (self *SBaseRegionDriver) RequestCreateInstanceBackup(ctx context.Context, guest *models.SGuest, ib *models.SInstanceBackup, task taskman.ITask, params *jsonutils.JSONDict) error { + return errors.Wrapf(cloudprovider.ErrNotImplemented, "RequestCreateInstanceBackup") +} + +func (self *SBaseRegionDriver) RequestDeleteInstanceBackup(ctx context.Context, ib *models.SInstanceBackup, task taskman.ITask) error { + return errors.Wrapf(cloudprovider.ErrNotImplemented, "RequestDeleteInstanceBackup") +} + func (self *SBaseRegionDriver) ValidateCreateCdnData(ctx context.Context, userCred mcclient.TokenCredential, input api.CDNDomainCreateInput) (api.CDNDomainCreateInput, error) { return input, errors.Wrapf(cloudprovider.ErrNotImplemented, "ValidateCreateCdnData") } diff --git a/pkg/compute/regiondrivers/kvm.go b/pkg/compute/regiondrivers/kvm.go index cefcacd9c6..bd91f9d492 100644 --- a/pkg/compute/regiondrivers/kvm.go +++ b/pkg/compute/regiondrivers/kvm.go @@ -1024,6 +1024,28 @@ func (self *SKVMRegionDriver) RequestDeleteInstanceSnapshot(ctx context.Context, return nil } +func (self *SKVMRegionDriver) RequestDeleteInstanceBackup(ctx context.Context, ib *models.SInstanceBackup, task taskman.ITask) error { + backups, err := ib.GetBackups() + if err != nil { + return err + } + if len(backups) == 0 { + task.SetStage("OnInstanceBackupDelete", nil) + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + return nil, nil + }) + return nil + } + params := jsonutils.NewDict() + params.Set("del_backup_id", jsonutils.NewString(backups[0].Id)) + task.SetStage("OnKvmDiskBackupDelete", params) + err = backups[0].StartBackupDeleteTask(ctx, task.GetUserCred(), task.GetTaskId()) + if err != nil { + return err + } + return nil +} + func (self *SKVMRegionDriver) RequestResetToInstanceSnapshot(ctx context.Context, guest *models.SGuest, isp *models.SInstanceSnapshot, task taskman.ITask, params *jsonutils.JSONDict) error { disks, _ := guest.GetGuestDisks() diskIndexI64, err := params.Int("disk_index") @@ -1232,6 +1254,83 @@ func (self *SKVMRegionDriver) RequestSyncDiskStatus(ctx context.Context, userCre return nil } +func (self *SKVMRegionDriver) RequestCreateInstanceBackup(ctx context.Context, guest *models.SGuest, ib *models.SInstanceBackup, task taskman.ITask, params *jsonutils.JSONDict) error { + disks, _ := guest.GetGuestDisks() + task.SetStage("OnKvmDisksSnapshot", params) + for i := range disks { + disk := disks[i] + backup, err := func() (*models.SDiskBackup, error) { + lockman.LockClass(ctx, models.DiskBackupManager, "name") + defer lockman.ReleaseClass(ctx, models.DiskBackupManager, "name") + + diskBackupName, err := db.GenerateName(ctx, models.DiskBackupManager, task.GetUserCred(), + fmt.Sprintf("%s-%s", ib.Name, rand.String(8))) + if err != nil { + return nil, errors.Wrap(err, "Generate diskbackup name") + } + + return models.DiskBackupManager.CreateBackup(ctx, task.GetUserCred(), disk.DiskId, ib.BackupStorageId, diskBackupName) + }() + if err != nil { + return err + } + err = models.InstanceBackupJointManager.CreateJoint(ctx, ib.Id, backup.Id, int8(i)) + if err != nil { + return err + } + taskParams := jsonutils.NewDict() + taskParams.Set("only_snapshot", jsonutils.JSONTrue) + if err := backup.StartBackupCreateTask(ctx, task.GetUserCred(), taskParams, task.GetTaskId()); err != nil { + return err + } + } + return nil +} + +func (self *SKVMRegionDriver) RequestSyncDiskBackupStatus(ctx context.Context, userCred mcclient.TokenCredential, backup *models.SDiskBackup, task taskman.ITask) error { + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + originStatus, _ := task.GetParams().GetString("origin_status") + if utils.IsInStringArray(originStatus, []string{api.BACKUP_STATUS_CREATING, api.BACKUP_STATUS_SNAPSHOT, api.BACKUP_STATUS_SAVING, api.BACKUP_STATUS_CLEANUP_SNAPSHOT, api.BACKUP_STATUS_DELETING}) { + return nil, backup.SetStatus(userCred, originStatus, "sync status") + } + backupStroage, err := backup.GetBackupStorage() + if err != nil { + return nil, errors.Wrap(err, "unable to get backupStorage") + } + storage, _ := backup.GetStorage() + var host *models.SHost + if storage != nil { + host = storage.GetMasterHost() + } + if host == nil { + host, err = models.HostManager.GetEnabledHost() + if err != nil { + return nil, errors.Wrap(err, "unable to GetEnabledHost") + } + } + log.Infof("host: %s, ManagerUri: %s", host.GetId(), host.ManagerUri) + url := fmt.Sprintf("%s/storages/sync-backup", host.ManagerUri) + body := jsonutils.NewDict() + body.Set("backup_id", jsonutils.NewString(backup.GetId())) + body.Set("backup_storage_id", jsonutils.NewString(backupStroage.GetId())) + body.Set("backup_storage_access_info", jsonutils.Marshal(backupStroage.AccessInfo)) + header := task.GetTaskRequestHeader() + _, res, err := httputils.JSONRequest(httputils.GetDefaultClient(), ctx, "POST", url, header, body, false) + if err != nil { + return nil, err + } + var backupStatus string + status, _ := res.GetString("status") + if status == api.BACKUP_EXIST { + backupStatus = api.BACKUP_STATUS_READY + } else { + backupStatus = api.SNAPSHOT_UNKNOWN + } + return nil, backup.SetStatus(userCred, backupStatus, "sync status") + }) + return nil +} + func (self *SKVMRegionDriver) RequestSyncSnapshotStatus(ctx context.Context, userCred mcclient.TokenCredential, snapshot *models.SSnapshot, task taskman.ITask) error { taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { storage := snapshot.GetStorage() @@ -1417,6 +1516,67 @@ func (self *SKVMRegionDriver) GetMaxElasticcacheSecurityGroupCount() int { return 0 } +func (self *SKVMRegionDriver) RequestDeleteBackup(ctx context.Context, backup *models.SDiskBackup, task taskman.ITask) error { + backupStroage, err := backup.GetBackupStorage() + if err != nil { + return errors.Wrap(err, "unable to get backupStorage") + } + storage, _ := backup.GetStorage() + var host *models.SHost + if storage != nil { + host = storage.GetMasterHost() + } + if host == nil { + host, err = models.HostManager.GetEnabledHost() + if err != nil { + return errors.Wrap(err, "unable to GetEnabledHost") + } + } + url := fmt.Sprintf("%s/storages/delete-backup", host.ManagerUri) + body := jsonutils.NewDict() + body.Set("backup_id", jsonutils.NewString(backup.GetId())) + body.Set("backup_storage_id", jsonutils.NewString(backupStroage.GetId())) + body.Set("backup_storage_access_info", jsonutils.Marshal(backupStroage.AccessInfo)) + header := task.GetTaskRequestHeader() + _, _, err = httputils.JSONRequest(httputils.GetDefaultClient(), ctx, "POST", url, header, body, false) + if err != nil { + return errors.Wrap(err, "unable to backup") + } + return nil +} + +func (self *SKVMRegionDriver) RequestCreateBackup(ctx context.Context, backup *models.SDiskBackup, snapshotId string, task taskman.ITask) error { + backupStroage, err := backup.GetBackupStorage() + if err != nil { + return errors.Wrap(err, "unable to get backupStorage") + } + disk, err := backup.GetDisk() + if err != nil { + return errors.Wrap(err, "unable to get disk") + } + guest := disk.GetGuest() + if guest == nil { + return errors.Wrap(err, "unable to get guest") + } + storage, err := disk.GetStorage() + if err != nil { + return errors.Wrap(err, "unable to get storage") + } + host, _ := guest.GetHost() + url := fmt.Sprintf("%s/disks/%s/backup/%s", host.ManagerUri, storage.Id, disk.Id) + body := jsonutils.NewDict() + body.Set("snapshot_id", jsonutils.NewString(snapshotId)) + body.Set("backup_id", jsonutils.NewString(backup.GetId())) + body.Set("backup_storage_id", jsonutils.NewString(backupStroage.GetId())) + body.Set("backup_storage_access_info", jsonutils.Marshal(backupStroage.AccessInfo)) + header := task.GetTaskRequestHeader() + _, _, err = httputils.JSONRequest(httputils.GetDefaultClient(), ctx, "POST", url, header, body, false) + if err != nil { + return errors.Wrap(err, "unable to backup") + } + return nil +} + func (self *SKVMRegionDriver) RequestAssociatEip(ctx context.Context, userCred mcclient.TokenCredential, eip *models.SElasticip, input api.ElasticipAssociateInput, obj db.IStatusStandaloneModel, task taskman.ITask) error { taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { if input.InstanceType == api.EIP_ASSOCIATE_TYPE_SERVER { diff --git a/pkg/compute/regiondrivers/managedvirtual.go b/pkg/compute/regiondrivers/managedvirtual.go index ff41b9d178..078e958325 100644 --- a/pkg/compute/regiondrivers/managedvirtual.go +++ b/pkg/compute/regiondrivers/managedvirtual.go @@ -2861,6 +2861,10 @@ func (self *SManagedVirtualizationRegionDriver) RequestSyncDiskStatus(ctx contex return nil } +func (self *SManagedVirtualizationRegionDriver) RequestSyncDiskBackupStatus(ctx context.Context, userCred mcclient.TokenCredential, backup *models.SDiskBackup, task taskman.ITask) error { + return fmt.Errorf("Not Implement RequestSyncDiskBackupStatus") +} + func (self *SManagedVirtualizationRegionDriver) RequestSyncSnapshotStatus(ctx context.Context, userCred mcclient.TokenCredential, snapshot *models.SSnapshot, task taskman.ITask) error { taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { iRegion, err := snapshot.GetISnapshotRegion() diff --git a/pkg/compute/service/handlers.go b/pkg/compute/service/handlers.go index cd745987ca..40a38083e5 100644 --- a/pkg/compute/service/handlers.go +++ b/pkg/compute/service/handlers.go @@ -233,6 +233,10 @@ func InitHandlers(app *appsrv.Application) { models.KubeClusterManager, models.KubeNodeManager, models.KubeNodePoolManager, + + models.BackupStorageManager, + models.DiskBackupManager, + models.InstanceBackupManager, } { db.RegisterModelManager(manager) handler := db.NewModelHandler(manager) @@ -265,6 +269,7 @@ func InitHandlers(app *appsrv.Application) { models.DBInstanceSecgroupManager, models.ElasticcachesecgroupManager, models.InterVpcNetworkVpcManager, + models.InstanceBackupJointManager, } { db.RegisterModelManager(manager) handler := db.NewJointModelHandler(manager) diff --git a/pkg/compute/service/service.go b/pkg/compute/service/service.go index f32f545b98..05390cd289 100644 --- a/pkg/compute/service/service.go +++ b/pkg/compute/service/service.go @@ -26,10 +26,12 @@ import ( _ "yunion.io/x/sqlchemy/backends" api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/apis/identity" "yunion.io/x/onecloud/pkg/cloudcommon" app_common "yunion.io/x/onecloud/pkg/cloudcommon/app" "yunion.io/x/onecloud/pkg/cloudcommon/cronman" "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/cloudcommon/elect" "yunion.io/x/onecloud/pkg/cloudcommon/etcd" common_options "yunion.io/x/onecloud/pkg/cloudcommon/options" @@ -43,6 +45,7 @@ import ( "yunion.io/x/onecloud/pkg/compute/tasks" "yunion.io/x/onecloud/pkg/controller/autoscaling" "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient/auth" "yunion.io/x/onecloud/pkg/multicloud/esxi" _ "yunion.io/x/onecloud/pkg/multicloud/loader" ) @@ -64,7 +67,14 @@ func StartService() { }) common_options.StartOptionManager(opts, opts.ConfigSyncPeriodSeconds, api.SERVICE_TYPE, api.SERVICE_VERSION, options.OnOptionsChange) - err := esxi.InitEsxiConfig(opts.EsxiOptions) + serviceUrl, err := auth.GetServiceURL(api.SERVER_TYPE_V2, opts.Region, "", identity.EndpointInterfaceInternal) + if err != nil { + log.Fatalf("unable to get service url: %v", err) + } + log.Infof("serviceUrl: %s", serviceUrl) + taskman.SetServiceUrl(serviceUrl) + + err = esxi.InitEsxiConfig(opts.EsxiOptions) if err != nil { log.Fatalf("unable to init esxi configs: %v", err) } diff --git a/pkg/compute/tasks/disk_backup_create_task.go b/pkg/compute/tasks/disk_backup_create_task.go new file mode 100644 index 0000000000..0c1af70890 --- /dev/null +++ b/pkg/compute/tasks/disk_backup_create_task.go @@ -0,0 +1,197 @@ +// 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 tasks + +import ( + "context" + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" + "yunion.io/x/onecloud/pkg/util/rand" +) + +type DiskBackupCreateTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(DiskBackupCreateTask{}) +} + +func (self *DiskBackupCreateTask) taskFailed(ctx context.Context, backup *models.SDiskBackup, reason jsonutils.JSONObject, status string) { + snapshotId, _ := self.Params.GetString("snapshot_id") + if len(snapshotId) > 0 { + snapshotModel, err := models.SnapshotManager.FetchById(snapshotId) + if err != nil { + log.Errorf("unable to get snapshot %s: %s", snapshotId, err.Error()) + } else { + err := snapshotModel.(*models.SSnapshot).RealDelete(ctx, self.UserCred) + if err != nil { + log.Errorf("unable to delete snapshot %s: %s", snapshotId, err.Error()) + } + } + } + reasonStr, _ := reason.GetString() + backup.SetStatus(self.UserCred, status, reasonStr) + logclient.AddActionLogWithStartable(self, backup, logclient.ACT_CREATE, reason, self.UserCred, false) + self.SetStageFailed(ctx, reason) +} + +func (self *DiskBackupCreateTask) taksSuccess(ctx context.Context, backup *models.SDiskBackup, data *jsonutils.JSONDict) { + backup.SetStatus(self.UserCred, api.BACKUP_STATUS_READY, "") + logclient.AddActionLogWithStartable(self, backup, logclient.ACT_CREATE, backup.GetShortDesc(ctx), self.UserCred, true) + notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{ + Obj: backup, + Action: notifyclient.ActionCreate, + }) + self.SetStageComplete(ctx, data) +} + +func (self *DiskBackupCreateTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + backup := obj.(*models.SDiskBackup) + if self.Params.Contains("snapshot_id") { + self.OnSnapshot(ctx, backup, nil) + return + } + backup.SetStatus(self.UserCred, api.BACKUP_STATUS_SNAPSHOT, "") + snapshot, err := self.CreateSnapshot(ctx, backup) + if err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error()), api.BACKUP_STATUS_SNAPSHOT_FAILED) + } + params := jsonutils.NewDict() + params.Set("snapshot_id", jsonutils.NewString(snapshot.GetId())) + self.SetStage("OnSnapshot", params) + err = snapshot.StartSnapshotCreateTask(ctx, self.UserCred, nil, self.GetId()) + if err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error()), api.BACKUP_STATUS_SNAPSHOT_FAILED) + } +} + +func (self *DiskBackupCreateTask) OnSnapshot(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + snapshotId, _ := self.Params.GetString("snapshot_id") + if self.Params.Contains("only_snapshot") { + p := jsonutils.NewDict() + p.Set("snapshot_id", jsonutils.NewString(snapshotId)) + p.Set("disk_backup_id", jsonutils.NewString(backup.GetId())) + self.taksSuccess(ctx, backup, p) + return + } + backup.SetStatus(self.UserCred, api.BACKUP_STATUS_SAVING, "") + self.SetStage("OnSave", nil) + if err := backup.GetRegionDriver().RequestCreateBackup(ctx, backup, snapshotId, self); err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error()), api.BACKUP_STATUS_SAVE_FAILED) + } +} + +func (self *DiskBackupCreateTask) OnSnapshotFailed(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + // remove snapshot + self.taskFailed(ctx, backup, data, api.BACKUP_STATUS_SNAPSHOT_FAILED) +} + +func (self *DiskBackupCreateTask) OnSave(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + // cleanup snapshot + snapshotId, _ := self.Params.GetString("snapshot_id") + self.SetStage("OnCleanupSnapshot", nil) + snapshotModel, err := models.SnapshotManager.FetchById(snapshotId) + if err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error()), api.BACKUP_STATUS_CLEANUP_SNAPSHOT_FAILED) + return + } + log.Infof("data from RequestCreateBackup: %s", data) + sizeMb, _ := data.Int("size_mb") + db.Update(backup, func() error { + backup.SizeMb = int(sizeMb) + return nil + }) + snapshot := snapshotModel.(*models.SSnapshot) + err = snapshot.StartSnapshotDeleteTask(ctx, self.UserCred, false, self.GetId()) + if err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error()), api.BACKUP_STATUS_CLEANUP_SNAPSHOT_FAILED) + return + } +} + +func (self *DiskBackupCreateTask) OnSaveFailed(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + snapshotId, _ := self.Params.GetString("snapshot_id") + snapshotModel, err := models.SnapshotManager.FetchById(snapshotId) + if err != nil { + log.Errorf("unable to cleanup snapshot: %s", err.Error()) + self.taskFailed(ctx, backup, data, api.BACKUP_STATUS_SAVE_FAILED) + return + } + snapshot := snapshotModel.(*models.SSnapshot) + self.taskFailed(ctx, backup, data, api.BACKUP_STATUS_SAVE_FAILED) + err = snapshot.StartSnapshotDeleteTask(ctx, self.UserCred, false, self.GetId()) + if err != nil { + log.Errorf("unable to cleanup snapshot: %s", err.Error()) + self.taskFailed(ctx, backup, data, api.BACKUP_STATUS_SAVE_FAILED) + return + } + self.taskFailed(ctx, backup, data, api.BACKUP_STATUS_SAVE_FAILED) +} + +func (self *DiskBackupCreateTask) OnCleanupSnapshot(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + snapshotId, _ := self.Params.GetString("snapshot_id") + snapshotModel, err := models.SnapshotManager.FetchById(snapshotId) + if err != nil { + log.Errorf("unable to get snapshot %s: %s", snapshotId, err.Error()) + } else { + err := snapshotModel.(*models.SSnapshot).RealDelete(ctx, self.UserCred) + if err != nil { + log.Errorf("unable to delete snapshot %s: %s", snapshotId, err.Error()) + } + } + self.taksSuccess(ctx, backup, nil) +} + +func (self *DiskBackupCreateTask) OnCleanupSnapshotFailed(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + self.taskFailed(ctx, backup, data, api.BACKUP_STATUS_CLEANUP_SNAPSHOT_FAILED) +} + +func (self *DiskBackupCreateTask) CreateSnapshot(ctx context.Context, diskBackup *models.SDiskBackup) (*models.SSnapshot, error) { + disk, err := diskBackup.GetDisk() + if err != nil { + return nil, errors.Wrapf(err, "unable to getdisk of disk backup %s", disk.GetId()) + } + guest := disk.GetGuest() + snapshot, err := func() (*models.SSnapshot, error) { + lockman.LockClass(ctx, models.SnapshotManager, "name") + defer lockman.ReleaseClass(ctx, models.SnapshotManager, "name") + + snapshotName, err := db.GenerateName(ctx, models.SnapshotManager, self.GetUserCred(), + fmt.Sprintf("%s-%s", diskBackup.Name, rand.String(8))) + if err != nil { + return nil, errors.Wrap(err, "Generate snapshot name") + } + + return models.SnapshotManager.CreateSnapshot( + ctx, self.GetUserCred(), api.SNAPSHOT_MANUAL, disk.GetId(), + guest.Id, "", snapshotName, -1) + }() + if err != nil { + return nil, errors.Wrapf(err, "unable to create snapshot of disk %s", disk.GetId()) + } + return snapshot, nil +} diff --git a/pkg/compute/tasks/disk_backup_delete_task.go b/pkg/compute/tasks/disk_backup_delete_task.go new file mode 100644 index 0000000000..5f2fbce7b5 --- /dev/null +++ b/pkg/compute/tasks/disk_backup_delete_task.go @@ -0,0 +1,63 @@ +// 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 tasks + +import ( + "context" + + "yunion.io/x/jsonutils" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type DiskBackupDeleteTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(DiskBackupDeleteTask{}) +} + +func (self *DiskBackupDeleteTask) taskFailed(ctx context.Context, backup *models.SDiskBackup, reason jsonutils.JSONObject) { + reasonStr, _ := reason.GetString() + backup.SetStatus(self.UserCred, api.BACKUP_STATUS_DELETE_FAILED, reasonStr) + logclient.AddActionLogWithStartable(self, backup, logclient.ACT_DELETE, reason, self.UserCred, false) + self.SetStageFailed(ctx, reason) +} + +func (self *DiskBackupDeleteTask) taskSuccess(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + backup.RealDelete(ctx, self.UserCred) + self.SetStageComplete(ctx, nil) +} + +func (self *DiskBackupDeleteTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + backup := obj.(*models.SDiskBackup) + self.SetStage("OnDelete", nil) + if err := backup.GetRegionDriver().RequestDeleteBackup(ctx, backup, self); err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error())) + } +} + +func (self *DiskBackupDeleteTask) OnDelete(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + self.taskSuccess(ctx, backup, nil) +} + +func (self *DiskBackupDeleteTask) OnDeleteFailed(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + self.taskFailed(ctx, backup, data) +} diff --git a/pkg/compute/tasks/disk_backup_recovery_task.go b/pkg/compute/tasks/disk_backup_recovery_task.go new file mode 100644 index 0000000000..5e7b7f41de --- /dev/null +++ b/pkg/compute/tasks/disk_backup_recovery_task.go @@ -0,0 +1,97 @@ +package tasks + +import ( + "context" + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" + compute_modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type DiskBackupRecoveryTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(DiskBackupRecoveryTask{}) +} + +func (self *DiskBackupRecoveryTask) taskFaild(ctx context.Context, backup *models.SDiskBackup, reason jsonutils.JSONObject) { + reasonStr, _ := reason.GetString() + backup.SetStatus(self.UserCred, api.BACKUP_STATUS_RECOVERY_FAILED, reasonStr) + logclient.AddActionLogWithStartable(self, backup, logclient.ACT_RECOVERY, reason, self.UserCred, false) + self.SetStageFailed(ctx, reason) +} + +func (self *DiskBackupRecoveryTask) taskSuccess(ctx context.Context, backup *models.SDiskBackup, data *jsonutils.JSONDict) { + backup.SetStatus(self.UserCred, api.BACKUP_STATUS_READY, "") + logclient.AddActionLogWithStartable(self, backup, logclient.ACT_RECOVERY, nil, self.UserCred, true) + self.SetStageComplete(ctx, data) +} + +func (self *DiskBackupRecoveryTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + backup := obj.(*models.SDiskBackup) + diskName, _ := self.Params.GetString("disk_name") + if diskName == "" { + diskName, _ = backup.DiskConfig.GetString("name") + } + diskConfig := &api.DiskConfig{} + err := backup.DiskConfig.Unmarshal(diskConfig) + if err != nil { + self.taskFaild(ctx, backup, jsonutils.NewString(err.Error())) + return + } + diskConfig.ImageId = "" + diskConfig.SnapshotId = "" + diskConfig.BackupId = backup.GetId() + input := api.DiskCreateInput{} + input.GenerateName = diskName + input.Description = fmt.Sprintf("recovery from backup %s", backup.GetName()) + input.Hypervisor = api.HYPERVISOR_KVM + input.DiskConfig = diskConfig + + taskHeader := self.GetTaskRequestHeader() + session := auth.GetSession(ctx, self.UserCred, "", "") + log.Infof("task_notify_url: %s\ntask_id: %s\n", taskHeader.Get(mcclient.TASK_NOTIFY_URL), taskHeader.Get(mcclient.TASK_ID)) + session.Header.Set(mcclient.TASK_NOTIFY_URL, taskHeader.Get(mcclient.TASK_NOTIFY_URL)) + session.Header.Set(mcclient.TASK_ID, taskHeader.Get(mcclient.TASK_ID)) + diskData, err := compute_modules.Disks.Create(session, jsonutils.Marshal(input)) + if err != nil { + self.taskFaild(ctx, backup, jsonutils.NewString(err.Error())) + return + } + diskId, _ := diskData.GetString("id") + params := jsonutils.NewDict() + params.Set("disk_id", jsonutils.NewString(diskId)) + self.SetStage("OnCreateDisk", params) +} + +func (self *DiskBackupRecoveryTask) OnCreateDisk(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + diskId, _ := self.Params.GetString("disk_id") + disk := models.DiskManager.FetchDiskById(diskId) + if disk == nil { + self.taskFaild(ctx, backup, jsonutils.NewString(fmt.Sprintf("disk %s disappeared", diskId))) + return + } + imageId, _ := backup.DiskConfig.GetString("image_id") + snapshotId, _ := backup.DiskConfig.GetString("snapshot_id") + db.Update(disk, func() error { + disk.TemplateId = imageId + disk.SnapshotId = snapshotId + return nil + }) + self.taskSuccess(ctx, backup, nil) +} + +func (self *DiskBackupRecoveryTask) OnCreateDiskFailed(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + self.taskFaild(ctx, backup, data) +} diff --git a/pkg/compute/tasks/disk_backup_syncstatus_task.go b/pkg/compute/tasks/disk_backup_syncstatus_task.go new file mode 100644 index 0000000000..80002ade83 --- /dev/null +++ b/pkg/compute/tasks/disk_backup_syncstatus_task.go @@ -0,0 +1,58 @@ +// 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 tasks + +import ( + "context" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type DiskBackupSyncstatusTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(DiskBackupSyncstatusTask{}) +} + +func (self *DiskBackupSyncstatusTask) taskFailed(ctx context.Context, backup *models.SDiskBackup, err jsonutils.JSONObject) { + logclient.AddActionLogWithContext(ctx, backup, logclient.ACT_SYNC_STATUS, err, self.UserCred, false) + self.SetStageFailed(ctx, err) +} + +func (self *DiskBackupSyncstatusTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + backup := obj.(*models.SDiskBackup) + + self.SetStage("OnDiskBackupSyncStatus", nil) + err := backup.GetRegionDriver().RequestSyncDiskBackupStatus(ctx, self.GetUserCred(), backup, self) + if err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error())) + return + } +} + +func (self *DiskBackupSyncstatusTask) OnDiskBackupSyncStatus(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + self.SetStageComplete(ctx, nil) +} + +func (self *DiskBackupSyncstatusTask) OnDiskBackupSyncStatusFailed(ctx context.Context, backup *models.SDiskBackup, data jsonutils.JSONObject) { + self.taskFailed(ctx, backup, data) +} diff --git a/pkg/hostman/guestman/guesthandlers/guesthandler.go b/pkg/hostman/guestman/guesthandlers/guesthandler.go index b659261cbd..7772f1b6ff 100644 --- a/pkg/hostman/guestman/guesthandlers/guesthandler.go +++ b/pkg/hostman/guestman/guesthandlers/guesthandler.go @@ -21,6 +21,7 @@ import ( "strings" "yunion.io/x/jsonutils" + "yunion.io/x/log" "yunion.io/x/pkg/errors" computeapi "yunion.io/x/onecloud/pkg/apis/compute" @@ -488,6 +489,7 @@ func guestSnapshot(ctx context.Context, sid string, body jsonutils.JSONObject) ( if !ok { return nil, httperrors.NewNotFoundError("guest %s not found", sid) } + log.Infof("guest info: %s", jsonutils.Marshal(guest)) var disk storageman.IDisk disks, _ := guest.Desc.GetArray("disks") diff --git a/pkg/hostman/guestman/guesthelper.go b/pkg/hostman/guestman/guesthelper.go index 7cd9344c51..d7c7e3d64b 100644 --- a/pkg/hostman/guestman/guesthelper.go +++ b/pkg/hostman/guestman/guesthelper.go @@ -83,6 +83,13 @@ type SDiskSnapshot struct { Disk storageman.IDisk } +type SDiskBackup struct { + Sid string + SnapshotId string + BackupId string + Disk storageman.IDisk +} + type SDeleteDiskSnapshot struct { Sid string DeleteSnapshot string diff --git a/pkg/hostman/options/options.go b/pkg/hostman/options/options.go index 6bc7bd765f..5cc4754b10 100644 --- a/pkg/hostman/options/options.go +++ b/pkg/hostman/options/options.go @@ -160,6 +160,8 @@ type SHostOptions struct { EnableVmUuid bool `help:"enable vm UUID" default:"true" json:"enable_vm_uuid"` EnableVirtioRngDevice bool `help:"enable qemu virtio-rng device" default:"false"` + + RestrictQemuImgConvertWorker bool `help:"restrict qemu-img convert worker" default:"false"` } var ( diff --git a/pkg/hostman/storageman/backupstorage/backup_storage.go b/pkg/hostman/storageman/backupstorage/backup_storage.go new file mode 100644 index 0000000000..897c0b7a02 --- /dev/null +++ b/pkg/hostman/storageman/backupstorage/backup_storage.go @@ -0,0 +1,44 @@ +// 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 backupstorage + +import ( + "fmt" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/util/qemuimg" +) + +type IBackupStorage interface { + CopyBackupFrom(srcFilename string, bakcupId string) error + CopyBackupTo(targetFilename string, backupId string) error + RemoveBackup(backupId string) error + IsExists(backupId string) (bool, error) + ConvertTo(destPath string, format qemuimg.TImageFormat, backupId string) error + ConvertFrom(srcPath string, format qemuimg.TImageFormat, backupId string) (int, error) +} + +func NewBackupStorage(backupStroageId string, backupStorageAccessInfo *jsonutils.JSONDict) (IBackupStorage, error) { + nfsHost, err := backupStorageAccessInfo.GetString("nfs_host") + if err != nil { + return nil, fmt.Errorf("need nfs_host in backup_storage_access_info") + } + nfsSharedDir, err := backupStorageAccessInfo.GetString("nfs_shared_dir") + if err != nil { + return nil, fmt.Errorf("need nfs_shared_dir in backup_storage_access_info") + } + return NewNFSBackupStorage(backupStroageId, nfsHost, nfsSharedDir), nil +} diff --git a/pkg/hostman/storageman/backupstorage/backup_storage_nfs.go b/pkg/hostman/storageman/backupstorage/backup_storage_nfs.go new file mode 100644 index 0000000000..e2079fc7b2 --- /dev/null +++ b/pkg/hostman/storageman/backupstorage/backup_storage_nfs.go @@ -0,0 +1,222 @@ +// 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 backupstorage + +import ( + "context" + "fmt" + "path" + "time" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" + "yunion.io/x/onecloud/pkg/hostman/options" + "yunion.io/x/onecloud/pkg/util/fileutils2" + "yunion.io/x/onecloud/pkg/util/procutils" + "yunion.io/x/onecloud/pkg/util/qemuimg" +) + +const BackupStoragePath = "/opt/cloud/workspace/backupstorage" + +type SNFSBackupStorage struct { + BackupStorageId string + Path string + NfsHost string + NfsSharedDir string +} + +func NewNFSBackupStorage(backupStorageId, nfsHost, nfsSharedDir string) *SNFSBackupStorage { + return &SNFSBackupStorage{ + BackupStorageId: backupStorageId, + NfsHost: nfsHost, + NfsSharedDir: nfsSharedDir, + Path: path.Join(BackupStoragePath, backupStorageId), + } +} + +func (s *SNFSBackupStorage) getBackupDir() string { + return path.Join(s.Path, "backups") +} + +func (s *SNFSBackupStorage) checkAndMount() error { + lockman.LockRawObject(context.Background(), "backupstorage", s.BackupStorageId) + defer lockman.ReleaseRawObject(context.Background(), "backupstorage", s.BackupStorageId) + if !fileutils2.Exists(s.Path) { + output, err := procutils.NewCommand("mkdir", "-p", s.Path).Output() + if err != nil { + log.Errorf("mkdir %s failed: %s", s.Path, output) + return errors.Wrapf(err, "mkdir %s failed: %s", s.Path, output) + } + } + if err := procutils.NewRemoteCommandAsFarAsPossible("mountpoint", s.Path).Run(); err == nil { + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err := procutils.NewRemoteCommandContextAsFarAsPossible(ctx, + "mount", "-t", "nfs", fmt.Sprintf("%s:%s", s.NfsHost, s.NfsSharedDir), s.Path).Run() + if err != nil { + return err + } + backupDir := s.getBackupDir() + if !fileutils2.Exists(backupDir) { + output, err := procutils.NewCommand("mkdir", "-p", backupDir).Output() + if err != nil { + log.Errorf("mkdir %s failed: %s", backupDir, output) + return errors.Wrapf(err, "mkdir %s failed: %s", backupDir, output) + } + } + return nil +} + +func (s *SNFSBackupStorage) unMount() error { + lockman.LockRawObject(context.Background(), "backupstorage", s.BackupStorageId) + defer lockman.ReleaseRawObject(context.Background(), "backupstorage", s.BackupStorageId) + out, err := procutils.NewRemoteCommandAsFarAsPossible("umount", s.Path).Output() + if err != nil { + return errors.Wrapf(err, "umount %s failed %s", s.Path, out) + } + return nil +} + +func (s *SNFSBackupStorage) CopyBackupFrom(srcFilename string, backupId string) error { + err := s.checkAndMount() + if err != nil { + return errors.Wrap(err, "unable to checkAndMount") + } + defer s.unMount() + backupDir := s.getBackupDir() + targetFilename := path.Join(backupDir, backupId) + if output, err := procutils.NewCommand("cp", srcFilename, targetFilename).Output(); err != nil { + log.Errorf("unable to cp %s to %s: %s", srcFilename, targetFilename, output) + return errors.Wrapf(err, "cp %s to %s failed and output is %q", srcFilename, targetFilename, output) + } + return nil +} + +func (s *SNFSBackupStorage) CopyBackupTo(targetFilename string, backupId string) error { + err := s.checkAndMount() + if err != nil { + return errors.Wrap(err, "unable to checkAndMount") + } + defer s.unMount() + backupDir := s.getBackupDir() + srcFilename := path.Join(backupDir, backupId) + if output, err := procutils.NewCommand("cp", srcFilename, targetFilename).Output(); err != nil { + log.Errorf("unable to cp %s to %s: %s", srcFilename, targetFilename, output) + return errors.Wrapf(err, "cp %s to %s failed and output is %q", srcFilename, targetFilename, output) + } + return nil +} + +func (s *SNFSBackupStorage) ConvertFrom(srcPath string, format qemuimg.TImageFormat, backupId string) (int, error) { + err := s.checkAndMount() + if err != nil { + return 0, errors.Wrap(err, "unable to checkAndMount") + } + defer s.unMount() + backupDir := s.getBackupDir() + destPath := path.Join(backupDir, backupId) + srcInfo := qemuimg.SConvertInfo{ + Path: srcPath, + Format: qemuimg.RAW, + IoLevel: qemuimg.IONiceNone, + Password: "", + } + destInfo := qemuimg.SConvertInfo{ + Path: destPath, + Format: qemuimg.QCOW2, + IoLevel: qemuimg.IONiceNone, + Password: "", + } + err = qemuimg.Convert(srcInfo, destInfo, nil, true, nil) + if err != nil { + return 0, err + } + newImage, err := qemuimg.NewQemuImage(destPath) + if err != nil { + return 0, err + } + return newImage.GetActualSizeMB(), nil +} + +func (s *SNFSBackupStorage) ConvertTo(destPath string, format qemuimg.TImageFormat, backupId string) error { + err := s.checkAndMount() + if err != nil { + return errors.Wrap(err, "unable to checkAndMount") + } + defer s.unMount() + backupDir := s.getBackupDir() + srcPath := path.Join(backupDir, backupId) + srcInfo := qemuimg.SConvertInfo{ + Path: srcPath, + Format: qemuimg.QCOW2, + IoLevel: qemuimg.IONiceNone, + Password: "", + } + destInfo := qemuimg.SConvertInfo{ + Path: destPath, + Format: format, + IoLevel: qemuimg.IONiceNone, + Password: "", + } + var opts []string + if format == qemuimg.QCOW2 { + opts = qemuimg.Qcow2SparseOptions() + } + var workerOpts []string + if options.HostOptions.RestrictQemuImgConvertWorker { + workerOpts = nil + } else { + workerOpts = []string{"-W", "-m", "16"} + } + return qemuimg.Convert(srcInfo, destInfo, opts, false, workerOpts) +} + +func (s *SNFSBackupStorage) GetBackupPath(backupId string) string { + backupDir := s.getBackupDir() + return path.Join(backupDir, backupId) +} + +func (s *SNFSBackupStorage) RemoveBackup(backupId string) error { + err := s.checkAndMount() + if err != nil { + return errors.Wrap(err, "unable to checkAndMount") + } + defer s.unMount() + backupDir := s.getBackupDir() + filename := path.Join(backupDir, backupId) + if !fileutils2.Exists(filename) { + return nil + } + if output, err := procutils.NewCommand("rm", filename).Output(); err != nil { + log.Errorf("unable to rm %s: %s", filename, output) + return errors.Wrapf(err, "rm %s failed and output is %q", filename, output) + } + return nil +} + +func (s *SNFSBackupStorage) IsExists(backupId string) (bool, error) { + err := s.checkAndMount() + if err != nil { + return false, errors.Wrap(err, "unable to checkAndMount") + } + defer s.unMount() + backupDir := s.getBackupDir() + filename := path.Join(backupDir, backupId) + return fileutils2.Exists(filename), nil +} diff --git a/pkg/hostman/storageman/backupstorage/doc.go b/pkg/hostman/storageman/backupstorage/doc.go new file mode 100644 index 0000000000..6edb8a97bd --- /dev/null +++ b/pkg/hostman/storageman/backupstorage/doc.go @@ -0,0 +1,15 @@ +// 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 backupstorage // import "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage" diff --git a/pkg/hostman/storageman/disk_base.go b/pkg/hostman/storageman/disk_base.go index 73144d0bcd..c9e709f3be 100644 --- a/pkg/hostman/storageman/disk_base.go +++ b/pkg/hostman/storageman/disk_base.go @@ -63,6 +63,9 @@ type IDisk interface { DeleteSnapshot(snapshotId, convertSnapshot string, pendingDelete bool) error DeployGuestFs(diskPath string, guestDesc *jsonutils.JSONDict, deployInfo *deployapi.DeployInfo) (jsonutils.JSONObject, error) + + GetBackupDir() string + DiskBackup(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) } type SBaseDisk struct { @@ -169,6 +172,10 @@ func (d *SBaseDisk) DiskSnapshot(ctx context.Context, params interface{}) (jsonu return nil, fmt.Errorf("Not implement disk.DiskSnapshot") } +func (d *SBaseDisk) DiskBackup(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { + return nil, fmt.Errorf("Not implement disk.DiskDeleteSnapshot") +} + func (d *SBaseDisk) DiskDeleteSnapshot(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { return nil, fmt.Errorf("Not implement disk.DiskDeleteSnapshot") } @@ -180,3 +187,7 @@ func (d *SBaseDisk) CreateFromRbdSnapshot(ctx context.Context, napshotUrl, srcDi func (d *SBaseDisk) DoDeleteSnapshot(snapshotId string) error { return fmt.Errorf("Not implement disk.DoDeleteSnapshot") } + +func (d *SBaseDisk) GetBackupDir() string { + return "" +} diff --git a/pkg/hostman/storageman/disk_local.go b/pkg/hostman/storageman/disk_local.go index acb739c815..4c582d5235 100644 --- a/pkg/hostman/storageman/disk_local.go +++ b/pkg/hostman/storageman/disk_local.go @@ -78,6 +78,10 @@ func (d *SLocalDisk) GetSnapshotDir() string { return path.Join(d.Storage.GetSnapshotDir(), d.Id+options.HostOptions.SnapshotDirSuffix) } +func (d *SLocalDisk) GetBackupDir() string { + return d.Storage.GetBackupDir() +} + func (d *SLocalDisk) GetSnapshotLocation() string { return d.GetSnapshotDir() } @@ -341,8 +345,45 @@ func (d *SLocalDisk) PostCreateFromImageFuse() { } } +func (d *SLocalDisk) DiskBackup(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { + diskBakcup := params.(*SDiskBakcup) + snapshotDir := d.GetSnapshotDir() + snapshotPath := path.Join(snapshotDir, diskBakcup.SnapshotId) + backupDir := d.GetBackupDir() + if !fileutils2.Exists(backupDir) { + output, err := procutils.NewCommand("mkdir", "-p", backupDir).Output() + if err != nil { + log.Errorf("mkdir %s failed: %s", backupDir, output) + return nil, errors.Wrapf(err, "mkdir %s failed: %s", backupDir, output) + } + } + backupPath := path.Join(backupDir, diskBakcup.BackupId) + img, err := qemuimg.NewQemuImage(snapshotPath) + if err != nil { + log.Errorln(err) + procutils.NewCommand("mv", "-f", backupPath, d.getPath()).Run() + return nil, err + } + newImage, err := img.Clone(backupPath, qemuimg.QCOW2, true) + if err != nil { + return nil, errors.Wrap(err, "unable to backup snapshot") + } + _, err = d.Storage.StorageBackup(ctx, &SStorageBackup{ + BackupId: diskBakcup.BackupId, + BackupStorageId: diskBakcup.BackupStorageId, + BackupStorageAccessInfo: diskBakcup.BackupStorageAccessInfo, + }) + if err != nil { + return nil, errors.Wrap(err, "unable to SStorageBackup") + } + data := jsonutils.NewDict() + data.Set("size_mb", jsonutils.NewInt(int64(newImage.GetActualSizeMB()))) + return data, nil +} + func (d *SLocalDisk) CreateSnapshot(snapshotId string) error { snapshotDir := d.GetSnapshotDir() + log.Infof("snapshotDir of LocalDisk %s: %s", d.Id, snapshotDir) if !fileutils2.Exists(snapshotDir) { output, err := procutils.NewCommand("mkdir", "-p", snapshotDir).Output() if err != nil { diff --git a/pkg/hostman/storageman/disk_rbd.go b/pkg/hostman/storageman/disk_rbd.go index 77f2070abd..25d522061b 100644 --- a/pkg/hostman/storageman/disk_rbd.go +++ b/pkg/hostman/storageman/disk_rbd.go @@ -221,6 +221,19 @@ func (d *SRBDDisk) PostCreateFromImageFuse() { log.Errorf("Not support PostCreateFromImageFuse") } +func (d *SRBDDisk) DiskBackup(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { + diskBackup := params.(*SDiskBakcup) + storage := d.Storage.(*SRbdStorage) + pool, _ := storage.StorageConf.GetString("pool") + sizeMb, err := storage.createBackup(pool, d.Id, diskBackup.SnapshotId, diskBackup.BackupId, diskBackup.BackupStorageId, diskBackup.BackupStorageAccessInfo) + if err != nil { + return nil, err + } + data := jsonutils.NewDict() + data.Set("size_mb", jsonutils.NewInt(int64(sizeMb))) + return data, nil +} + func (d *SRBDDisk) CreateSnapshot(snapshotId string) error { storage := d.Storage.(*SRbdStorage) pool, _ := storage.StorageConf.GetString("pool") diff --git a/pkg/hostman/storageman/diskhandlers/diskhandler.go b/pkg/hostman/storageman/diskhandlers/diskhandler.go index 5db6498e52..e3d9b957da 100644 --- a/pkg/hostman/storageman/diskhandlers/diskhandler.go +++ b/pkg/hostman/storageman/diskhandlers/diskhandler.go @@ -48,6 +48,7 @@ var ( "snapshot": diskSnapshot, "delete-snapshot": diskDeleteSnapshot, "cleanup-snapshots": diskCleanupSnapshots, + "backup": diskBackup, } ) @@ -325,6 +326,74 @@ func diskSnapshot(ctx context.Context, storage storageman.IStorage, diskId strin return nil, nil } +func diskStorageBackup(ctx context.Context, storage storageman.IStorage, diskId string, disk storageman.IDisk, body jsonutils.JSONObject) (interface{}, error) { + backupId, err := body.GetString("backup_id") + if err != nil { + return nil, httperrors.NewMissingParameterError("backup_id") + } + backupStorageId, err := body.GetString("backup_storage_id") + if err != nil { + return nil, httperrors.NewMissingParameterError("backup_storage_id") + } + backupStorageAccessInfo, err := body.Get("backup_storage_access_info") + if err != nil { + return nil, httperrors.NewMissingParameterError("backup_storage_access_info") + } + hostutils.DelayTask(ctx, storage.StorageBackup, &storageman.SStorageBackup{ + BackupId: backupId, + BackupStorageId: backupStorageId, + BackupStorageAccessInfo: backupStorageAccessInfo.(*jsonutils.JSONDict), + }) + return nil, nil +} + +func diskStorageBackupRecovery(ctx context.Context, storage storageman.IStorage, diskId string, disk storageman.IDisk, body jsonutils.JSONObject) (interface{}, error) { + backupId, err := body.GetString("backup_id") + if err != nil { + return nil, httperrors.NewMissingParameterError("backup_id") + } + backupStorageId, err := body.GetString("backup_storage_id") + if err != nil { + return nil, httperrors.NewMissingParameterError("backup_storage_id") + } + backupStorageAccessInfo, err := body.Get("backup_storage_access_info") + if err != nil { + return nil, httperrors.NewMissingParameterError("backup_storage_access_info") + } + hostutils.DelayTask(ctx, storage.StorageBackupRecovery, storageman.SStorageBackup{ + BackupId: backupId, + BackupStorageId: backupStorageId, + BackupStorageAccessInfo: backupStorageAccessInfo.(*jsonutils.JSONDict), + }) + return nil, nil +} + +func diskBackup(ctx context.Context, storage storageman.IStorage, diskId string, disk storageman.IDisk, body jsonutils.JSONObject) (interface{}, error) { + snapshotId, err := body.GetString("snapshot_id") + if err != nil { + return nil, httperrors.NewMissingParameterError("snapshot_id") + } + backupId, err := body.GetString("backup_id") + if err != nil { + return nil, httperrors.NewMissingParameterError("backup_id") + } + backupStorageId, err := body.GetString("backup_storage_id") + if err != nil { + return nil, httperrors.NewMissingParameterError("backup_storage_id") + } + backupStorageAccessInfo, err := body.Get("backup_storage_access_info") + if err != nil { + return nil, httperrors.NewMissingParameterError("backup_storage_access_info") + } + hostutils.DelayTask(ctx, disk.DiskBackup, &storageman.SDiskBakcup{ + BackupId: backupId, + SnapshotId: snapshotId, + BackupStorageId: backupStorageId, + BackupStorageAccessInfo: backupStorageAccessInfo.(*jsonutils.JSONDict), + }) + return nil, nil +} + func diskDeleteSnapshot(ctx context.Context, storage storageman.IStorage, diskId string, disk storageman.IDisk, body jsonutils.JSONObject) (interface{}, error) { snapshotId, err := body.GetString("snapshot_id") if err != nil { diff --git a/pkg/hostman/storageman/storage_base.go b/pkg/hostman/storageman/storage_base.go index 095eea2075..440d26b6b9 100644 --- a/pkg/hostman/storageman/storage_base.go +++ b/pkg/hostman/storageman/storage_base.go @@ -46,6 +46,7 @@ const ( _RECYCLE_BIN_ = "recycle_bin" _IMGSAVE_BACKUPS_ = "imgsave_backups" _SNAPSHOT_PATH_ = "snapshots" + _BACKUP_PATH_ = "backups" ErrStorageTimeout = constError("storage accessible check timeout") TempBindMountPath = "/opt/cloud/workspace/temp-bind" @@ -102,6 +103,10 @@ type IStorage interface { DeleteSnapshots(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) IsSnapshotExist(diskId, snapshotId string) (bool, error) + GetBackupDir() string + StorageBackup(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) + StorageBackupRecovery(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) + GetFreeSizeMb() int GetCapacity() int @@ -117,6 +122,8 @@ type IStorage interface { SaveToGlance(context.Context, interface{}) (jsonutils.JSONObject, error) CreateDiskFromSnapshot(context.Context, IDisk, *SDiskCreateByDiskinfo) error + CreateDiskFromBackup(context.Context, IDisk, *SDiskCreateByDiskinfo) error + // GetCloneTargetDiskPath generate target disk path by target disk id GetCloneTargetDiskPath(ctx context.Context, targetDiskId string) string // CloneDiskFromStorage clone disk from other storage @@ -174,6 +181,14 @@ func (s *SBaseStorage) SetStoragecacheId(storagecacheId string) { s.StoragecacheId = storagecacheId } +func (s *SBaseStorage) StorageBackup(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { + return nil, nil +} + +func (s *SBaseStorage) StorageBackupRecovery(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { + return nil, nil +} + func (s *SBaseStorage) GetName(generateName func() string) string { if len(s.StorageName) > 0 { return s.StorageName @@ -329,6 +344,9 @@ func (s *SBaseStorage) CreateDiskByDiskinfo(ctx context.Context, params interfac case len(createParams.DiskInfo.ImageId) > 0: log.Infof("CreateDiskFromTemplate %s", createParams) return s.CreateDiskFromTemplate(ctx, disk, createParams) + case createParams.DiskInfo.Backup != nil: + log.Infof("CreateDiskFromBackup %s", createParams) + return s.createDiskFromBackup(ctx, disk, createParams) case createParams.DiskInfo.DiskSizeMb > 0: log.Infof("CreateRawDisk %s", createParams) return s.CreateRawDisk(ctx, disk, createParams) @@ -363,6 +381,20 @@ func (s *SBaseStorage) CreateDiskFromSnpashot(ctx context.Context, disk IDisk, i return disk.GetDiskDesc(), nil } +func (s *SBaseStorage) createDiskFromBackup(ctx context.Context, disk IDisk, input *SDiskCreateByDiskinfo) (jsonutils.JSONObject, error) { + var storage = input.Storage + if input.DiskInfo.Backup == nil { + return nil, httperrors.NewMissingParameterError("Backup") + } + + err := storage.CreateDiskFromBackup(ctx, disk, input) + if err != nil { + return nil, errors.Wrapf(err, "CreateDiskFromBackup") + } + + return disk.GetDiskDesc(), nil +} + func (s *SBaseStorage) DestinationPrepareMigrate( ctx context.Context, liveMigrate bool, disksUri string, snapshotsUri string, disksBackingFile, srcSnapshots jsonutils.JSONObject, rebaseDisks bool, diskinfo jsonutils.JSONObject, serverId string, idx, totalDiskCount int, @@ -378,6 +410,22 @@ func (s *SBaseStorage) CloneDiskFromStorage(ctx context.Context, srcStorage ISto return nil, httperrors.ErrNotImplemented } +func (s *SBaseStorage) GetBackupDir() string { + return path.Join(s.Path, _BACKUP_PATH_) +} + +func (s *SBaseStorage) CreateDiskFromBackup(ctx context.Context, disk IDisk, input *SDiskCreateByDiskinfo) error { + info := input.DiskInfo + backupPath := path.Join(s.GetBackupDir(), info.Backup.BackupId) + img, err := qemuimg.NewQemuImage(backupPath) + if err != nil { + log.Errorln("unable to new qemu image for %s: %s", backupPath, err.Error()) + return err + } + _, err = img.Clone(disk.GetPath(), qemuimg.QCOW2, false) + return err +} + /*************************Background delete snapshot job****************************/ func StartSnapshotRecycle(storage IStorage) { diff --git a/pkg/hostman/storageman/storage_local.go b/pkg/hostman/storageman/storage_local.go index 34d903857f..16025a3a02 100644 --- a/pkg/hostman/storageman/storage_local.go +++ b/pkg/hostman/storageman/storage_local.go @@ -36,6 +36,7 @@ import ( "yunion.io/x/onecloud/pkg/hostman/hostutils" "yunion.io/x/onecloud/pkg/hostman/hostutils/kubelet" "yunion.io/x/onecloud/pkg/hostman/options" + "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage" "yunion.io/x/onecloud/pkg/hostman/storageman/remotefile" "yunion.io/x/onecloud/pkg/httperrors" modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" @@ -99,6 +100,61 @@ func (s *SLocalStorage) SyncStorageSize() error { s.StorageId, content) return err } +func (s *SLocalStorage) CreateDiskFromBackup(ctx context.Context, disk IDisk, input *SDiskCreateByDiskinfo) error { + info := input.DiskInfo + backupPath := path.Join(s.GetBackupDir(), info.Backup.BackupId) + if !fileutils2.Exists(backupPath) { + _, err := s.storageBackupRecovery(ctx, &SStorageBackup{ + BackupId: input.DiskInfo.Backup.BackupId, + BackupStorageId: input.DiskInfo.Backup.BackupStorageId, + BackupStorageAccessInfo: input.DiskInfo.Backup.BackupStorageAccessInfo.Copy(), + }) + if err != nil { + return errors.Wrap(err, "unable to storageBackupRecovery") + } + } + img, err := qemuimg.NewQemuImage(backupPath) + if err != nil { + log.Errorln("unable to new qemu image for %s: %s", backupPath, err.Error()) + return err + } + _, err = img.Clone(disk.GetPath(), qemuimg.QCOW2, false) + return err +} + +func (s *SLocalStorage) StorageBackup(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { + sbParams := params.(*SStorageBackup) + backupStorage, err := backupstorage.NewBackupStorage(sbParams.BackupStorageId, sbParams.BackupStorageAccessInfo) + if err != nil { + return nil, err + } + backupPath := path.Join(s.GetBackupDir(), sbParams.BackupId) + err = backupStorage.CopyBackupFrom(backupPath, sbParams.BackupId) + if err != nil { + return nil, err + } + // remove local backup + output, err := procutils.NewCommand("rm", backupPath).Output() + if err != nil { + log.Errorf("rm %s failed %s", backupPath, output) + return nil, errors.Wrapf(err, "rm %s failed %s", backupPath, output) + } + return nil, nil +} + +func (s *SLocalStorage) storageBackupRecovery(ctx context.Context, sbParams *SStorageBackup) (jsonutils.JSONObject, error) { + backupStorage, err := backupstorage.NewBackupStorage(sbParams.BackupStorageId, sbParams.BackupStorageAccessInfo) + if err != nil { + return nil, err + } + backupPath := path.Join(s.GetBackupDir(), sbParams.BackupId) + return nil, backupStorage.CopyBackupTo(backupPath, sbParams.BackupId) +} + +func (s *SLocalStorage) StorageBackupRecovery(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { + sbParams := params.(*SStorageBackup) + return s.storageBackupRecovery(ctx, sbParams) +} func (s *SLocalStorage) GetAvailSizeMb() int { sizeMb := s.SBaseStorage.GetAvailSizeMb() diff --git a/pkg/hostman/storageman/storage_rbd.go b/pkg/hostman/storageman/storage_rbd.go index e64dc8271a..f6ce86b163 100644 --- a/pkg/hostman/storageman/storage_rbd.go +++ b/pkg/hostman/storageman/storage_rbd.go @@ -33,6 +33,7 @@ import ( "yunion.io/x/onecloud/pkg/hostman/hostdeployer/deployclient" "yunion.io/x/onecloud/pkg/hostman/hostutils" "yunion.io/x/onecloud/pkg/hostman/options" + "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage" modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" "yunion.io/x/onecloud/pkg/mcclient/modules/image" "yunion.io/x/onecloud/pkg/util/cephutils" @@ -290,6 +291,45 @@ func (s *SRbdStorage) resetDisk(pool string, diskId string, snapshotId string) e return snap.Rollback() } +func (s *SRbdStorage) createBackup(pool string, diskId string, snapshotId string, backupId string, backupStorageId string, backupStorageAccessInfo *jsonutils.JSONDict) (int, error) { + client, err := s.GetClient() + if err != nil { + return 0, errors.Wrapf(err, "GetClient") + } + client.SetPool(pool) + defer client.Close() + img, err := client.GetImage(diskId) + if err != nil { + return 0, errors.Wrapf(err, "GetImage") + } + snap, err := img.GetSnapshot(snapshotId) + if err != nil { + return 0, errors.Wrapf(err, "unable to GetSnapshot %s of Image %s", snapshotId, diskId) + } + backupName := fmt.Sprintf("backup_%s", backupId) + err = snap.Clone(pool, backupName) + if err != nil { + return 0, errors.Wrapf(err, "unable to Clone snap %s", fmt.Sprintf("%s@%s", diskId, snapshotId)) + } + backupImg, err := client.GetImage(backupName) + if err != nil { + return 0, errors.Wrapf(err, "GetImage") + } + defer backupImg.Delete() + // convert backupStorage + backupStorage, err := backupstorage.NewBackupStorage(backupStorageId, backupStorageAccessInfo) + if err != nil { + return 0, errors.Wrap(err, "unable to NewNFSBackupStorage") + } + srcPath := fmt.Sprintf("rbd:%s/%s%s", pool, backupName, s.getStorageConfString()) + // convert + sizeMb, err := backupStorage.ConvertFrom(srcPath, qemuimg.RAW, backupId) + if err != nil { + return 0, errors.Wrapf(err, "unable to ConvertFrom with srcPath %s and format %s", srcPath, qemuimg.RAW.String()) + } + return sizeMb, nil +} + func (s *SRbdStorage) createSnapshot(pool string, diskId string, snapshotId string) error { client, err := s.GetClient() if err != nil { @@ -522,6 +562,25 @@ func (s *SRbdStorage) CreateDiskFromSnapshot(ctx context.Context, disk IDisk, in return disk.CreateFromRbdSnapshot(ctx, info.SnapshotUrl, info.SrcDiskId, info.SrcPool) } +func (s *SRbdStorage) GetBackupDir() string { + return "" +} + +func (s *SRbdStorage) CreateDiskFromBackup(ctx context.Context, disk IDisk, input *SDiskCreateByDiskinfo) error { + backup := input.DiskInfo.Backup + pool, _ := s.StorageConf.GetString("pool") + destPath := fmt.Sprintf("rbd:%s/%s%s", pool, disk.GetId(), s.getStorageConfString()) + backupStorage, err := backupstorage.NewBackupStorage(backup.BackupStorageId, backup.BackupStorageAccessInfo) + if err != nil { + return errors.Wrap(err, "unable to NewNFSBackupStorage") + } + err = backupStorage.ConvertTo(destPath, qemuimg.RAW, backup.BackupId) + if err != nil { + return errors.Wrapf(err, "unable to Convert to with destPath %s and format %s", destPath, qemuimg.RAW.String()) + } + return nil +} + func (s *SRbdStorage) getDiskPath(diskId string) string { storageConf := s.GetStorageConf() pool, _ := storageConf.GetString("pool") diff --git a/pkg/hostman/storageman/storagehandler/storagehandler.go b/pkg/hostman/storageman/storagehandler/storagehandler.go index aa82b408b1..c0e719128f 100644 --- a/pkg/hostman/storageman/storagehandler/storagehandler.go +++ b/pkg/hostman/storageman/storagehandler/storagehandler.go @@ -23,9 +23,11 @@ import ( "yunion.io/x/log" "yunion.io/x/pkg/errors" + "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/hostman/hostutils" "yunion.io/x/onecloud/pkg/hostman/storageman" + "yunion.io/x/onecloud/pkg/hostman/storageman/backupstorage" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient/auth" modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" @@ -57,6 +59,12 @@ func AddStorageHandler(prefix string, app *appsrv.Application) { app.AddHandler("GET", fmt.Sprintf("%s/%s/is-mount-point", prefix, keyWords), auth.Authenticate(storageVerifyMountPoint)) + app.AddHandler("POST", + fmt.Sprintf("%s/%s/delete-backup", prefix, keyWords), + auth.Authenticate(storageDeleteBackup)) + app.AddHandler("POST", + fmt.Sprintf("%s/%s/sync-backup", prefix, keyWords), + auth.Authenticate(storageSyncBackup)) } } @@ -183,6 +191,87 @@ func storageUpdate(ctx context.Context, body jsonutils.JSONObject) (interface{}, return nil, nil } +func storageSyncBackup(ctx context.Context, w http.ResponseWriter, r *http.Request) { + _, _, body := appsrv.FetchEnv(ctx, w, r) + backupId, err := body.GetString("backup_id") + if err != nil { + hostutils.Response(ctx, w, httperrors.NewMissingParameterError("backup_id")) + return + } + backupStorageId, err := body.GetString("backup_storage_id") + if err != nil { + hostutils.Response(ctx, w, httperrors.NewMissingParameterError("backup_storage_id")) + return + } + backupStorageAccessInfo, err := body.Get("backup_storage_access_info") + if err != nil { + hostutils.Response(ctx, w, httperrors.NewMissingParameterError("backup_storage_access_info")) + return + } + backupStorage, err := backupstorage.NewBackupStorage(backupStorageId, backupStorageAccessInfo.(*jsonutils.JSONDict)) + if err != nil { + hostutils.Response(ctx, w, err) + return + } + exist, err := backupStorage.IsExists(backupId) + if err != nil { + hostutils.Response(ctx, w, err) + return + } + var ( + ret = jsonutils.NewDict() + status string + ) + if exist { + status = compute.BACKUP_EXIST + } else if !exist && err == nil { + status = compute.BACKUP_NOT_EXIST + } else { + log.Errorf("fetch snapshot exist failed %s", err) + status = compute.BACKUP_STATUS_UNKNOWN + } + ret.Set("status", jsonutils.NewString(status)) + hostutils.Response(ctx, w, ret) +} + +func storageDeleteBackup(ctx context.Context, w http.ResponseWriter, r *http.Request) { + _, _, body := appsrv.FetchEnv(ctx, w, r) + backupId, err := body.GetString("backup_id") + if err != nil { + hostutils.Response(ctx, w, httperrors.NewMissingParameterError("backup_id")) + return + } + backupStorageId, err := body.GetString("backup_storage_id") + if err != nil { + hostutils.Response(ctx, w, httperrors.NewMissingParameterError("backup_storage_id")) + return + } + backupStorageAccessInfo, err := body.Get("backup_storage_access_info") + if err != nil { + hostutils.Response(ctx, w, httperrors.NewMissingParameterError("backup_storage_access_info")) + return + } + hostutils.DelayTask(ctx, deleteBackup, &storageman.SStorageBackup{ + BackupId: backupId, + BackupStorageId: backupStorageId, + BackupStorageAccessInfo: backupStorageAccessInfo.(*jsonutils.JSONDict), + }) + hostutils.ResponseOk(ctx, w) +} + +func deleteBackup(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { + sbParams := params.(*storageman.SStorageBackup) + backupStorage, err := backupstorage.NewBackupStorage(sbParams.BackupStorageId, sbParams.BackupStorageAccessInfo) + if err != nil { + return nil, err + } + err = backupStorage.RemoveBackup(sbParams.BackupId) + if err != nil { + return nil, err + } + return nil, nil +} + func storageDeleteSnapshots(ctx context.Context, w http.ResponseWriter, r *http.Request) { params, _, body := appsrv.FetchEnv(ctx, w, r) var storageId = params[""] diff --git a/pkg/hostman/storageman/storagehelper.go b/pkg/hostman/storageman/storagehelper.go index 6c2da23ce1..721ee03679 100644 --- a/pkg/hostman/storageman/storagehelper.go +++ b/pkg/hostman/storageman/storagehelper.go @@ -43,3 +43,16 @@ type SDiskCleanupSnapshots struct { ConvertSnapshots []jsonutils.JSONObject DeleteSnapshots []jsonutils.JSONObject } + +type SDiskBakcup struct { + SnapshotId string + BackupId string + BackupStorageId string + BackupStorageAccessInfo *jsonutils.JSONDict +} + +type SStorageBackup struct { + BackupId string + BackupStorageId string + BackupStorageAccessInfo *jsonutils.JSONDict +} diff --git a/pkg/mcclient/options/disks.go b/pkg/mcclient/options/disks.go index 8d22340aa3..33364d43bf 100644 --- a/pkg/mcclient/options/disks.go +++ b/pkg/mcclient/options/disks.go @@ -34,6 +34,8 @@ type DiskCreateOptions struct { Backend string `help:"Backend of this disk"` Schedtag []string `help:"Schedule policy, key = aggregate name, value = require|exclude|prefer|avoid" metavar:""` TaskNotify bool `help:"Setup task notify"` + SnapshotId string `help:"snapshot id"` + BackupId string `help:"backupid"` } func (o DiskCreateOptions) Params() (*api.DiskCreateInput, error) { @@ -65,5 +67,6 @@ func (o DiskCreateOptions) Params() (*api.DiskCreateInput, error) { if o.Storage != "" { params.Storage = o.Storage } + params.BackupId = o.BackupId return params, nil } diff --git a/pkg/util/logclient/consts.go b/pkg/util/logclient/consts.go index 099cc75517..833f34525d 100644 --- a/pkg/util/logclient/consts.go +++ b/pkg/util/logclient/consts.go @@ -214,4 +214,6 @@ const ( ACT_MERGE_NETWORK = "merge_network" ACT_RESTART_NETWORK = "restart_network" + + ACT_RECOVERY = "recovery" ) diff --git a/pkg/util/qemuimg/init.go b/pkg/util/qemuimg/init.go index b18575f4c3..457e87a915 100644 --- a/pkg/util/qemuimg/init.go +++ b/pkg/util/qemuimg/init.go @@ -68,3 +68,7 @@ func qcow2SparseOptions() []string { return []string{} } } + +func Qcow2SparseOptions() []string { + return qcow2SparseOptions() +} diff --git a/pkg/util/qemuimg/qemuimg.go b/pkg/util/qemuimg/qemuimg.go index c300fb494b..6a389bc02a 100644 --- a/pkg/util/qemuimg/qemuimg.go +++ b/pkg/util/qemuimg/qemuimg.go @@ -174,21 +174,29 @@ func (img *SQemuImage) IsChained() bool { return len(img.BackFilePath) > 0 } -func (img *SQemuImage) doConvert(name string, format TImageFormat, options []string, compact bool, password string) error { - if !img.IsValid() { - return fmt.Errorf("self is not valid") - } - cmdline := []string{"-c", strconv.Itoa(int(img.IoLevel)), +type SConvertInfo struct { + Path string + Format TImageFormat + IoLevel TIONiceLevel + Password string +} + +func Convert(srcInfo, destInfo SConvertInfo, options []string, compact bool, workerOpions []string) error { + cmdline := []string{"-c", strconv.Itoa(int(srcInfo.IoLevel)), qemutils.GetQemuImg(), "convert"} if compact { cmdline = append(cmdline, "-c") } - // https://bugzilla.redhat.com/show_bug.cgi?id=1969848 - // https://bugs.launchpad.net/qemu/+bug/1805256 - // qemu-img convert may hang on aarch64, fix: add -m 1 - cmdline = append(cmdline, "-m", "1") - cmdline = append(cmdline, "-f", img.Format.String(), "-O", format.String()) - if len(password) > 0 { + if workerOpions == nil { + // https://bugzilla.redhat.com/show_bug.cgi?id=1969848 + // https://bugs.launchpad.net/qemu/+bug/1805256 + // qemu-img convert may hang on aarch64, fix: add -m 1 + cmdline = append(cmdline, "-m", "1") + } else { + cmdline = append(cmdline, workerOpions...) + } + cmdline = append(cmdline, "-f", srcInfo.Format.String(), "-O", destInfo.Format.String()) + if len(destInfo.Password) > 0 { if options == nil { options = make([]string, 0) } @@ -197,12 +205,12 @@ func (img *SQemuImage) doConvert(name string, format TImageFormat, options []str if len(options) > 0 { cmdline = append(cmdline, "-o", strings.Join(options, ",")) } - cmdline = append(cmdline, img.Path, name) + cmdline = append(cmdline, srcInfo.Path, destInfo.Path) log.Infof("XXXX qemu-img command: %s", cmdline) cmd := procutils.NewRemoteCommandAsFarAsPossible("ionice", cmdline...) var stdin io.WriteCloser var err error - if len(img.Password) > 0 || len(password) > 0 { + if len(srcInfo.Password) > 0 || len(destInfo.Password) > 0 { stdin, err = cmd.StdinPipe() if err != nil { return errors.Wrap(err, "convert stdin") @@ -212,25 +220,41 @@ func (img *SQemuImage) doConvert(name string, format TImageFormat, options []str if err != nil { return errors.Wrap(err, "do convert") } - if len(img.Password) > 0 || len(password) > 0 { + if len(srcInfo.Password) > 0 || len(destInfo.Password) > 0 { input := "" - if len(img.Password) > 0 { - input = fmt.Sprintf("%s%s\r", input, img.Password) + if len(srcInfo.Password) > 0 { + input = fmt.Sprintf("%s%s\r", input, srcInfo.Password) } - if len(password) > 0 { - input = fmt.Sprintf("%s%s\r", input, password) + if len(destInfo.Password) > 0 { + input = fmt.Sprintf("%s%s\r", input, destInfo.Password) } io.WriteString(stdin, input+"\n") } err = cmd.Wait() if err != nil { log.Errorf("clone fail %s", err) - os.Remove(name) + os.Remove(destInfo.Path) return err } return nil } +func (img *SQemuImage) doConvert(name string, format TImageFormat, options []string, compact bool, password string) error { + if !img.IsValid() { + return fmt.Errorf("self is not valid") + } + return Convert(SConvertInfo{ + Path: img.Path, + Format: img.Format, + IoLevel: img.IoLevel, + Password: img.Password, + }, SConvertInfo{ + Path: name, + Format: format, + Password: password, + }, options, compact, nil) +} + func (img *SQemuImage) Clone(name string, format TImageFormat, compact bool) (*SQemuImage, error) { switch format { case QCOW2: From 3502a8074f5733dd86abb0641dbcd6622ff7d565 Mon Sep 17 00:00:00 2001 From: rainzm Date: Wed, 19 Jan 2022 11:28:59 +0800 Subject: [PATCH 3/6] feat(region): support instance backup Instance backup is a logical collection of multiple disk backups --- pkg/apis/compute/guest_const.go | 3 + pkg/compute/models/guest_actions.go | 71 +++ pkg/compute/models/guests.go | 37 ++ pkg/compute/models/instance_backup.go | 437 ++++++++++++++++++ pkg/compute/models/instance_backup_joint.go | 84 ++++ pkg/compute/models/instance_snapshot_joint.go | 5 + .../tasks/instance_backup_create_task.go | 134 ++++++ .../tasks/instance_backup_delete_task.go | 94 ++++ .../tasks/instance_backup_recovery_task.go | 113 +++++ 9 files changed, 978 insertions(+) create mode 100644 pkg/compute/models/instance_backup.go create mode 100644 pkg/compute/models/instance_backup_joint.go create mode 100644 pkg/compute/tasks/instance_backup_create_task.go create mode 100644 pkg/compute/tasks/instance_backup_delete_task.go create mode 100644 pkg/compute/tasks/instance_backup_recovery_task.go diff --git a/pkg/apis/compute/guest_const.go b/pkg/apis/compute/guest_const.go index 6a7b4c59b9..6957f812c0 100644 --- a/pkg/apis/compute/guest_const.go +++ b/pkg/apis/compute/guest_const.go @@ -96,6 +96,9 @@ const ( VM_SNAPSHOT_RESET_FAILED = "snapshot_reset_failed" VM_SNAPSHOT_AND_CLONE_FAILED = "clone_from_snapshot_failed" + VM_START_INSTANCE_BACKUP = "start_instance_backup" + VM_INSTANCE_BACKUP_FAILED = "instance_backup_failed" + VM_SYNC_CONFIG = "sync_config" VM_SYNC_FAIL = "sync_fail" diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index 5193a15895..07628d507e 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -4562,6 +4562,45 @@ func (self *SGuest) validateCreateInstanceSnapshot( return pendingUsage, nil } +func (self *SGuest) validateCreateInstanceBackup( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + data jsonutils.JSONObject, +) error { + if !utils.IsInStringArray(self.Hypervisor, []string{api.HYPERVISOR_KVM}) { + return httperrors.NewBadRequestError("guest hypervisor %s can't create instance snapshot", self.Hypervisor) + } + + if len(self.BackupHostId) > 0 { + return httperrors.NewBadRequestError("Can't do instance snapshot with backup guest") + } + + if !utils.IsInStringArray(self.Status, []string{api.VM_RUNNING, api.VM_READY}) { + return httperrors.NewInvalidStatusError("guest can't do snapshot in status %s", self.Status) + } + + var name string + ownerId := self.GetOwnerId() + dataDict := data.(*jsonutils.JSONDict) + nameHint, err := dataDict.GetString("generate_name") + if err == nil { + name, err = db.GenerateName(ctx, InstanceBackupManager, ownerId, nameHint) + if err != nil { + return err + } + dataDict.Set("name", jsonutils.NewString(name)) + } else if name, err = dataDict.GetString("name"); err != nil { + return httperrors.NewMissingParameterError("name") + } + + err = db.NewNameValidator(InstanceBackupManager, ownerId, name, nil) + if err != nil { + return err + } + return nil +} + // 1. validate guest status, guest hypervisor // 2. validate every disk manual snapshot count // 3. validate snapshot quota with disk count @@ -4590,6 +4629,33 @@ func (self *SGuest) PerformInstanceSnapshot( return nil, nil } +func (self *SGuest) PerformInstanceBackup(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + lockman.LockClass(ctx, InstanceSnapshotManager, userCred.GetProjectId()) + defer lockman.ReleaseClass(ctx, InstanceSnapshotManager, userCred.GetProjectId()) + err := self.validateCreateInstanceBackup(ctx, userCred, query, data) + if err != nil { + return nil, err + } + name, _ := data.GetString("name") + backupStorageId, _ := data.GetString("backup_storage_id") + if backupStorageId == "" { + return nil, httperrors.NewMissingParameterError("backup_storage_id") + } + _, err = BackupStorageManager.FetchById(backupStorageId) + if err == sql.ErrNoRows { + return nil, httperrors.NewInputParameterError("unkown backup_storage_id %s", backupStorageId) + } + instanceBackup, err := InstanceBackupManager.CreateInstanceBackup(ctx, userCred, self, name, backupStorageId) + if err != nil { + return nil, httperrors.NewInternalServerError("create instance backup failed: %s", err) + } + err = self.InstanceCreateBackup(ctx, userCred, instanceBackup) + if err != nil { + return nil, httperrors.NewInternalServerError("start create backup task failed: %s", err) + } + return nil, nil +} + func (self *SGuest) InstaceCreateSnapshot( ctx context.Context, userCred mcclient.TokenCredential, @@ -4600,6 +4666,11 @@ func (self *SGuest) InstaceCreateSnapshot( return instanceSnapshot.StartCreateInstanceSnapshotTask(ctx, userCred, pendingUsage, "") } +func (self *SGuest) InstanceCreateBackup(ctx context.Context, userCred mcclient.TokenCredential, instanceBackup *SInstanceBackup) error { + self.SetStatus(userCred, api.VM_START_INSTANCE_BACKUP, "instance backup") + return instanceBackup.StartCreateInstanceBackupTask(ctx, userCred, "") +} + func (self *SGuest) PerformInstanceSnapshotReset(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ServerResetInput) (jsonutils.JSONObject, error) { if self.Status != api.VM_READY { diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 9c70966a98..8f527d4944 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -1084,6 +1084,25 @@ func parseInstanceSnapshot(input *api.ServerCreateInput) (*api.ServerCreateInput return input, nil } +func parseInstanceBackup(input *api.ServerCreateInput) (*api.ServerCreateInput, error) { + ispi, err := InstanceBackupManager.FetchByIdOrName(nil, input.InstanceBackupId) + if err == sql.ErrNoRows { + return nil, httperrors.NewBadRequestError("can't find instance backup %s", input.InstanceBackupId) + } + if err != nil { + return nil, httperrors.NewInternalServerError("fetch instance backup error %s", err) + } + isp := ispi.(*SInstanceBackup) + if isp.Status != api.INSTANCE_BACKUP_STATUS_READY && isp.Status != api.INSTANCE_BACKUP_STATUS_RECOVERY { + return nil, httperrors.NewBadRequestError("Instance backup not ready") + } + input, err = isp.ToInstanceCreateInput(input) + if len(input.Disks) == 0 { + return nil, httperrors.NewInputParameterError("there are no disks in this instance backup, try another one") + } + return input, nil +} + func (manager *SGuestManager) validateCreateData( ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*api.ServerCreateInput, error) { @@ -1115,6 +1134,24 @@ func (manager *SGuestManager) validateCreateData( input.InstanceType = inputInstaceType } + if len(input.InstanceBackupId) > 0 { + inputMem := input.VmemSize + inputCpu := input.VcpuCount + inputInstaceType := input.InstanceType + input, err = parseInstanceBackup(input) + if err != nil { + return nil, err + } + // keep input cpu mem flavor + if inputMem > 0 { + input.VmemSize = inputMem + } + if inputMem > 0 { + input.VcpuCount = inputCpu + } + input.InstanceType = inputInstaceType + } + resetPassword := true if input.ResetPassword != nil { resetPassword = *input.ResetPassword diff --git a/pkg/compute/models/instance_backup.go b/pkg/compute/models/instance_backup.go new file mode 100644 index 0000000000..5e8f702336 --- /dev/null +++ b/pkg/compute/models/instance_backup.go @@ -0,0 +1,437 @@ +// 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 models + +import ( + "context" + "database/sql" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/utils" + "yunion.io/x/sqlchemy" + + api "yunion.io/x/onecloud/pkg/apis/compute" + schedapi "yunion.io/x/onecloud/pkg/apis/scheduler" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +func init() { + InstanceBackupManager = &SInstanceBackupManager{ + SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager( + SInstanceBackup{}, + "instancebackups_tbl", + "instancebackup", + "instancebackups", + ), + } + InstanceBackupManager.SetVirtualObject(InstanceBackupManager) +} + +type SInstanceBackup struct { + db.SVirtualResourceBase + + SManagedResourceBase + SCloudregionResourceBase + db.SMultiArchResourceBase + + BackupStorageId string `width:"36" charset:"ascii" nullable:"true" create:"required" list:"user" index:"true"` + + GuestId string `width:"36" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"` + // 云主机配置 + ServerConfig jsonutils.JSONObject `nullable:"true" list:"user"` + // 云主机标签 + ServerMetadata jsonutils.JSONObject `nullable:"true" list:"user"` + // 安全组 + SecGroups jsonutils.JSONObject `nullable:"true" list:"user"` + // 秘钥Id + KeypairId string `width:"36" charset:"ascii" nullable:"true" list:"user"` + // 操作系统类型 + OsType string `width:"36" charset:"ascii" nullable:"true" list:"user"` + // 套餐名称 + InstanceType string `width:"64" charset:"utf8" nullable:"true" list:"user" create:"optional"` + // 主机备份容量和 + SizeMb int `nullable:"false" list:"user"` +} + +type SInstanceBackupManager struct { + db.SVirtualResourceBaseManager + SManagedResourceBaseManager + SCloudregionResourceBaseManager + db.SMultiArchResourceBaseManager +} + +var InstanceBackupManager *SInstanceBackupManager + +func (manager *SInstanceBackupManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.InstanceBackupListInput) (*sqlchemy.SQuery, error) { + q, err := manager.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, query.VirtualResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter") + } + + q, err = manager.SManagedResourceBaseManager.ListItemFilter(ctx, q, userCred, query.ManagedResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SManagedResourceBaseManager.ListItemFilter") + } + + q, err = manager.SMultiArchResourceBaseManager.ListItemFilter(ctx, q, userCred, query.MultiArchResourceBaseListInput) + if err != nil { + return nil, errors.Wrap(err, "SMultiArchResourceBaseManager.ListItemFilter") + } + + guestStr := query.ServerId + if len(guestStr) > 0 { + guestObj, err := GuestManager.FetchByIdOrName(userCred, guestStr) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, httperrors.NewResourceNotFoundError2("guests", guestStr) + } else { + return nil, httperrors.NewGeneralError(err) + } + } + q = q.Equals("guest_id", guestObj.GetId()) + } + + if len(query.OsType) > 0 { + q = q.In("os_type", query.OsType) + } + + return q, nil +} + +func (manager *SInstanceBackupManager) OrderByExtraFields( + ctx context.Context, + q *sqlchemy.SQuery, + userCred mcclient.TokenCredential, + query api.InstanceBackupListInput, +) (*sqlchemy.SQuery, error) { + var err error + + q, err = manager.SVirtualResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.VirtualResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.OrderByExtraFields") + } + + q, err = manager.SManagedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.ManagedResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SManagedResourceBaseManager.OrderByExtraFields") + } + + return q, nil +} + +func (manager *SInstanceBackupManager) ListItemExportKeys(ctx context.Context, + q *sqlchemy.SQuery, + userCred mcclient.TokenCredential, + keys stringutils2.SSortedStrings, +) (*sqlchemy.SQuery, error) { + var err error + q, err = manager.SVirtualResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys) + if err != nil { + return nil, err + } + + return q, nil +} + +func (manager *SInstanceBackupManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { + var err error + + q, err = manager.SVirtualResourceBaseManager.QueryDistinctExtraField(q, field) + if err == nil { + return q, nil + } + + q, err = manager.SManagedResourceBaseManager.QueryDistinctExtraField(q, field) + if err == nil { + return q, nil + } + + return q, httperrors.ErrNotFound +} + +func (self *SInstanceBackup) GetGuest() (*SGuest, error) { + if len(self.GuestId) == 0 { + return nil, errors.ErrNotFound + } + guest := GuestManager.FetchGuestById(self.GuestId) + if guest == nil { + return nil, errors.ErrNotFound + } + return guest, nil +} + +func (self *SInstanceBackup) GetBackupStorage() (*SBackupStorage, error) { + ibs, err := BackupStorageManager.FetchById(self.BackupStorageId) + if err != nil { + return nil, err + } + return ibs.(*SBackupStorage), nil +} + +func (self *SInstanceBackup) getMoreDetails(userCred mcclient.TokenCredential, out api.InstanceBackupDetails) api.InstanceBackupDetails { + guest := GuestManager.FetchGuestById(self.GuestId) + if guest != nil { + out.Guest = guest.Name + out.GuestStatus = guest.Status + } + backupStorage, _ := self.GetBackupStorage() + if backupStorage != nil { + out.BackupStorageName = backupStorage.GetName() + } + return out +} + +func (manager *SInstanceBackupManager) FetchCustomizeColumns( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + objs []interface{}, + fields stringutils2.SSortedStrings, + isList bool, +) []api.InstanceBackupDetails { + rows := make([]api.InstanceBackupDetails, len(objs)) + + virtRows := manager.SVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + manRows := manager.SManagedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + + for i := range rows { + rows[i] = api.InstanceBackupDetails{ + VirtualResourceDetails: virtRows[i], + ManagedResourceInfo: manRows[i], + } + rows[i] = objs[i].(*SInstanceBackup).getMoreDetails(userCred, rows[i]) + } + + return rows +} + +func (self *SInstanceBackup) StartCreateInstanceBackupTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error { + if task, err := taskman.TaskManager.NewTask(ctx, "InstanceBackupCreateTask", self, userCred, nil, parentTaskId, "", nil); err != nil { + return err + } else { + task.ScheduleRun(nil) + } + return nil +} + +func (manager *SInstanceBackupManager) fillInstanceBackup(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, instanceBackup *SInstanceBackup) { + instanceBackup.SetModelManager(manager, instanceBackup) + instanceBackup.ProjectId = guest.ProjectId + instanceBackup.DomainId = guest.DomainId + instanceBackup.GuestId = guest.Id + guestSchedInput := guest.ToSchedDesc() + + host, _ := guest.GetHost() + instanceBackup.ManagerId = host.ManagerId + zone, _ := host.GetZone() + instanceBackup.CloudregionId = zone.CloudregionId + + guestSchedInput.HostId = "" + guestSchedInput.Project = "" + guestSchedInput.Domain = "" + for i := 0; i < len(guestSchedInput.Networks); i++ { + guestSchedInput.Networks[i].Mac = "" + guestSchedInput.Networks[i].Address = "" + guestSchedInput.Networks[i].Address6 = "" + } + instanceBackup.ServerConfig = jsonutils.Marshal(guestSchedInput.ServerConfig) + if len(guest.KeypairId) > 0 { + instanceBackup.KeypairId = guest.KeypairId + } + serverMetadata := jsonutils.NewDict() + if loginAccount := guest.GetMetadata(ctx, "login_account", nil); len(loginAccount) > 0 { + loginKey := guest.GetMetadata(ctx, "login_key", nil) + if len(guest.KeypairId) == 0 && len(loginKey) > 0 { + passwd, e := utils.DescryptAESBase64(guest.Id, loginKey) + if e == nil { + serverMetadata.Set("login_account", jsonutils.NewString(loginAccount)) + serverMetadata.Set("passwd", jsonutils.NewString(passwd)) + } + } else { + serverMetadata.Set("login_key", jsonutils.NewString(loginKey)) + serverMetadata.Set("login_account", jsonutils.NewString(loginAccount)) + } + } + if osArch := guest.GetMetadata(ctx, "os_arch", nil); len(osArch) > 0 { + serverMetadata.Set("os_arch", jsonutils.NewString(osArch)) + } + if osDist := guest.GetMetadata(ctx, "os_distribution", nil); len(osDist) > 0 { + serverMetadata.Set("os_distribution", jsonutils.NewString(osDist)) + } + if osName := guest.GetMetadata(ctx, "os_name", nil); len(osName) > 0 { + serverMetadata.Set("os_name", jsonutils.NewString(osName)) + } + if osVersion := guest.GetMetadata(ctx, "os_version", nil); len(osVersion) > 0 { + serverMetadata.Set("os_version", jsonutils.NewString(osVersion)) + } + secs, _ := guest.GetSecgroups() + if len(secs) > 0 { + secIds := make([]string, len(secs)) + for i := 0; i < len(secs); i++ { + secIds[i] = secs[i].Id + } + instanceBackup.SecGroups = jsonutils.Marshal(secIds) + } + instanceBackup.OsType = guest.OsType + instanceBackup.OsArch = guest.OsArch + instanceBackup.ServerMetadata = serverMetadata + instanceBackup.InstanceType = guest.InstanceType +} + +func (manager *SInstanceBackupManager) CreateInstanceBackup(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, name, backupStorageId string) (*SInstanceBackup, error) { + instanceBackup := &SInstanceBackup{} + instanceBackup.SetModelManager(manager, instanceBackup) + instanceBackup.Name = name + instanceBackup.BackupStorageId = backupStorageId + manager.fillInstanceBackup(ctx, userCred, guest, instanceBackup) + // compute size of instanceBackup + //instanceBackup.SizeMb = guest.getDiskSize() + err := manager.TableSpec().Insert(ctx, instanceBackup) + if err != nil { + return nil, err + } + return instanceBackup, nil +} + +func (self *SInstanceBackup) ToInstanceCreateInput(sourceInput *api.ServerCreateInput) (*api.ServerCreateInput, error) { + + serverConfig := new(schedapi.ServerConfig) + if err := self.ServerConfig.Unmarshal(serverConfig); err != nil { + return nil, errors.Wrap(err, "unmarshal sched input") + } + + isjs := make([]SInstanceBackupJoint, 0) + err := InstanceBackupJointManager.Query().Equals("instance_backup_id", self.Id).Asc("disk_index").All(&isjs) + if err != nil { + return nil, errors.Wrap(err, "fetch instance backups") + } + + for i := 0; i < len(serverConfig.Disks); i++ { + index := serverConfig.Disks[i].Index + if index < len(isjs) { + serverConfig.Disks[i].BackupId = isjs[index].DiskBackupId + serverConfig.Disks[i].ImageId = "" + serverConfig.Disks[i].SnapshotId = "" + } + } + + sourceInput.Disks = serverConfig.Disks + if sourceInput.VmemSize == 0 { + sourceInput.VmemSize = serverConfig.Memory + } + if sourceInput.VcpuCount == 0 { + sourceInput.VcpuCount = serverConfig.Ncpu + } + if len(self.KeypairId) > 0 { + sourceInput.KeypairId = self.KeypairId + } + if self.SecGroups != nil { + secGroups := make([]string, 0) + inputSecgs := make([]string, 0) + self.SecGroups.Unmarshal(&secGroups) + for i := 0; i < len(secGroups); i++ { + _, err := SecurityGroupManager.FetchSecgroupById(secGroups[i]) + if err == nil { + inputSecgs = append(inputSecgs, secGroups[i]) + } + } + sourceInput.Secgroups = inputSecgs + } + sourceInput.OsType = self.OsType + sourceInput.InstanceType = self.InstanceType + if len(sourceInput.Networks) == 0 { + sourceInput.Networks = serverConfig.Networks + } + return sourceInput, nil +} + +func (self *SInstanceBackup) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error { + if self.Status == api.INSTANCE_SNAPSHOT_START_DELETE || self.Status == api.INSTANCE_SNAPSHOT_RESET { + return httperrors.NewForbiddenError("can't delete instance snapshot with wrong status") + } + return nil +} + +func (self *SInstanceBackup) CustomizeDelete( + ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, data jsonutils.JSONObject) error { + + return self.StartInstanceBackupDeleteTask(ctx, userCred, "") +} + +func (self *SInstanceBackup) StartInstanceBackupDeleteTask( + ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error { + + task, err := taskman.TaskManager.NewTask(ctx, "InstanceBackupDeleteTask", self, userCred, nil, parentTaskId, "", nil) + if err != nil { + log.Errorf("%s", err) + return err + } + self.SetStatus(userCred, api.INSTANCE_BACKUP_STATUS_DELETING, "InstanceBackupDeleteTask") + task.ScheduleRun(nil) + return nil +} + +func (self *SInstanceBackup) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error { + return db.DeleteModel(ctx, userCred, self) +} + +func (self *SInstanceBackup) Delete(ctx context.Context, userCred mcclient.TokenCredential) error { + return nil +} + +func (self *SInstanceBackup) GetRegionDriver() IRegionDriver { + provider := self.GetProviderName() + return GetRegionDriver(provider) +} + +func (self *SInstanceBackup) GetBackups() ([]SDiskBackup, error) { + isjq := InstanceBackupJointManager.Query().SubQuery() + backups := make([]SDiskBackup, 0) + dq := DiskBackupManager.Query() + q := dq.Join(isjq, sqlchemy.Equals(dq.Field("id"), isjq.Field("disk_backup_id"))).Filter( + sqlchemy.Equals(isjq.Field("instance_backup_id"), self.GetId())).Asc(isjq.Field("disk_index")) + err := db.FetchModelObjects(DiskBackupManager, q, &backups) + if err != nil { + return nil, errors.Wrap(err, "db.FetchModelObjects") + } + return backups, nil +} + +func (self *SInstanceBackup) PerformRecovery(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.InstanceBackupRecoveryInput) (jsonutils.JSONObject, error) { + return nil, self.StartRecoveryTask(ctx, userCred, "", input.Name) +} + +func (self *SInstanceBackup) StartRecoveryTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string, serverName string) error { + self.SetStatus(userCred, api.INSTANCE_BACKUP_STATUS_RECOVERY, "") + var params *jsonutils.JSONDict + if serverName != "" { + params = jsonutils.NewDict() + params.Set("server_name", jsonutils.NewString(serverName)) + } + task, err := taskman.TaskManager.NewTask(ctx, "InstanceBackupRecoveryTask", self, userCred, params, parentTaskId, "", nil) + if err != nil { + return err + } else { + task.ScheduleRun(nil) + } + return nil +} diff --git a/pkg/compute/models/instance_backup_joint.go b/pkg/compute/models/instance_backup_joint.go new file mode 100644 index 0000000000..c7a022b40d --- /dev/null +++ b/pkg/compute/models/instance_backup_joint.go @@ -0,0 +1,84 @@ +// 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 models + +import ( + "context" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/mcclient" +) + +func init() { + db.InitManager(func() { + InstanceBackupJointManager = &SInstanceBackupJointManager{ + SVirtualJointResourceBaseManager: db.NewVirtualJointResourceBaseManager( + SInstanceBackupJoint{}, + "instancebackupjoints_tbl", + "instancebackupjoint", + "instancebackupjoints", + InstanceBackupManager, + DiskBackupManager, + ), + } + InstanceBackupJointManager.SetVirtualObject(InstanceBackupJointManager) + }) +} + +// +onecloud:swagger-gen-ignore +type SInstanceBackupJoint struct { + db.SVirtualJointResourceBase + + InstanceBackupId string `width:"36" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"` + DiskBackupId string `width:"36" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"` + DiskIndex int8 `nullable:"false" default:"0" list:"user" create:"required"` +} + +// +onecloud:swagger-gen-ignore +type SInstanceBackupJointManager struct { + db.SVirtualJointResourceBaseManager +} + +func (manager *SInstanceBackupJointManager) GetMasterFieldName() string { + return "instance_backup_id" +} + +func (manager *SInstanceBackupJointManager) GetSlaveFieldName() string { + return "disk_backup_id" +} + +var InstanceBackupJointManager *SInstanceBackupJointManager + +func (manager *SInstanceBackupJointManager) CreateJoint(ctx context.Context, instanceBackupId, backupId string, diskIndex int8) error { + instanceBackupJoint := &SInstanceBackupJoint{} + instanceBackupJoint.SetModelManager(manager, instanceBackupJoint) + + instanceBackupJoint.InstanceBackupId = instanceBackupId + instanceBackupJoint.DiskBackupId = backupId + instanceBackupJoint.DiskIndex = diskIndex + return manager.TableSpec().Insert(ctx, instanceBackupJoint) +} + +func (manager *SInstanceBackupJointManager) IsSubBackup(backupId string) (bool, error) { + count, err := manager.Query().Equals("disk_backup_id", backupId).CountWithError() + if err != nil { + return false, err + } + return count > 0, nil +} + +func (self *SInstanceBackupJoint) Detach(ctx context.Context, userCred mcclient.TokenCredential) error { + return db.DetachJoint(ctx, userCred, self) +} diff --git a/pkg/compute/models/instance_snapshot_joint.go b/pkg/compute/models/instance_snapshot_joint.go index 3875194790..a940d5d7b8 100644 --- a/pkg/compute/models/instance_snapshot_joint.go +++ b/pkg/compute/models/instance_snapshot_joint.go @@ -18,6 +18,7 @@ import ( "context" "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/mcclient" ) func init() { @@ -77,3 +78,7 @@ func (manager *SInstanceSnapshotJointManager) IsSubSnapshot(snapshotId string) ( } return count > 0, nil } + +func (self *SInstanceSnapshotJoint) Detach(ctx context.Context, userCred mcclient.TokenCredential) error { + return db.DetachJoint(ctx, userCred, self) +} diff --git a/pkg/compute/tasks/instance_backup_create_task.go b/pkg/compute/tasks/instance_backup_create_task.go new file mode 100644 index 0000000000..8a21d143b0 --- /dev/null +++ b/pkg/compute/tasks/instance_backup_create_task.go @@ -0,0 +1,134 @@ +// 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 tasks + +import ( + "context" + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type InstanceBackupCreateTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(InstanceBackupCreateTask{}) +} + +func (self *InstanceBackupCreateTask) taskFailed(ctx context.Context, ib *models.SInstanceBackup, guest *models.SGuest, reason jsonutils.JSONObject) { + if guest != nil { + guest.SetStatus(self.UserCred, compute.VM_INSTANCE_BACKUP_FAILED, reason.String()) + } + reasonStr, _ := reason.GetString() + ib.SetStatus(self.UserCred, compute.INSTANCE_BACKUP_STATUS_CREATE_FAILED, reasonStr) + logclient.AddActionLogWithStartable(self, ib, logclient.ACT_CREATE, reason, self.UserCred, false) + self.SetStageFailed(ctx, reason) +} + +func (self *InstanceBackupCreateTask) taskSuccess(ctx context.Context, ib *models.SInstanceBackup) { + ib.SetStatus(self.UserCred, compute.INSTANCE_BACKUP_STATUS_READY, "") + logclient.AddActionLogWithStartable(self, ib, logclient.ACT_CREATE, nil, self.UserCred, true) + self.SetStageComplete(ctx, nil) +} + +func (self *InstanceBackupCreateTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + ib := obj.(*models.SInstanceBackup) + self.SetStage("OnInstanceBackup", nil) + guest := models.GuestManager.FetchGuestById(ib.GuestId) + params := jsonutils.NewDict() + if err := ib.GetRegionDriver().RequestCreateInstanceBackup(ctx, guest, ib, self, params); err != nil { + self.taskFailed(ctx, ib, guest, jsonutils.NewString(err.Error())) + } +} + +func (self *InstanceBackupCreateTask) OnKvmDisksSnapshot(ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + subTasks := taskman.SubTaskManager.GetTotalSubtasks(self.Id, "OnKvmDisksSnapshot", "") + guest := models.GuestManager.FetchGuestById(ib.GuestId) + self.SetStage("OnInstanceBackup", nil) + for i := range subTasks { + log.Infof("subsTask %s result: %s", subTasks[i].SubtaskId, subTasks[i].Result) + result, err := jsonutils.ParseString(subTasks[i].Result) + if err != nil { + self.taskFailed(ctx, ib, guest, jsonutils.NewString(fmt.Sprintf("unable to parse %s", subTasks[i].Result))) + return + } + if subTasks[i].Status == taskman.SUBTASK_FAIL { + self.taskFailed(ctx, ib, guest, result) + return + } + snapshotId, _ := result.GetString("snapshot_id") + diskBakcupId, _ := result.GetString("disk_backup_id") + ibackup, err := models.DiskBackupManager.FetchById(diskBakcupId) + if err != nil { + self.taskFailed(ctx, ib, guest, jsonutils.NewString(err.Error())) + return + } + backup := ibackup.(*models.SDiskBackup) + params := jsonutils.NewDict() + params.Set("snapshot_id", jsonutils.NewString(snapshotId)) + if err := backup.StartBackupCreateTask(ctx, self.UserCred, params, self.Id); err != nil { + self.taskFailed(ctx, ib, guest, jsonutils.NewString(err.Error())) + return + } + } + guest.StartSyncstatus(ctx, self.UserCred, "") +} + +func (self *InstanceBackupCreateTask) OnKvmDisksSnapshotFailed(ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + self.taskFailed(ctx, ib, nil, data) +} + +func (self *InstanceBackupCreateTask) OnInstanceBackup(ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + subTasks := taskman.SubTaskManager.GetTotalSubtasks(self.Id, "OnInstanceBackup", "") + for i := range subTasks { + if subTasks[i].Status == taskman.SUBTASK_SUCC { + continue + } + result, err := jsonutils.ParseString(subTasks[i].Result) + if err != nil { + self.taskFailed(ctx, ib, nil, jsonutils.NewString(fmt.Sprintf("unable to parse %s", subTasks[i].Result))) + return + } + self.taskFailed(ctx, ib, nil, result) + } + // update size_mb + backups, err := ib.GetBackups() + if err != nil { + self.taskFailed(ctx, ib, nil, jsonutils.NewString(err.Error())) + return + } + var sizeMb int + for i := range backups { + sizeMb += backups[i].SizeMb + } + db.Update(ib, func() error { + ib.SizeMb = sizeMb + return nil + }) + self.taskSuccess(ctx, ib) +} + +func (self *InstanceBackupCreateTask) OnInstanceBackupFailed(ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + self.taskFailed(ctx, ib, nil, data) +} diff --git a/pkg/compute/tasks/instance_backup_delete_task.go b/pkg/compute/tasks/instance_backup_delete_task.go new file mode 100644 index 0000000000..648315a98a --- /dev/null +++ b/pkg/compute/tasks/instance_backup_delete_task.go @@ -0,0 +1,94 @@ +// 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 tasks + +import ( + "context" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type InstanceBackupDeleteTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(InstanceBackupDeleteTask{}) +} + +func (self *InstanceBackupDeleteTask) taskFailed(ctx context.Context, ib *models.SInstanceBackup, reason jsonutils.JSONObject) { + reasonStr, _ := reason.GetString() + ib.SetStatus(self.UserCred, compute.INSTANCE_BACKUP_STATUS_DELETE_FAILED, reasonStr) + logclient.AddActionLogWithStartable(self, ib, logclient.ACT_CREATE, reason, self.UserCred, false) + self.SetStageFailed(ctx, reason) +} + +func (self *InstanceBackupDeleteTask) taskSuccess(ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + ib.RealDelete(ctx, self.UserCred) + logclient.AddActionLogWithContext(ctx, ib, logclient.ACT_DELETE, nil, self.UserCred, true) + self.SetStageComplete(ctx, nil) +} + +func (self *InstanceBackupDeleteTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + ib := obj.(*models.SInstanceBackup) + self.SetStage("OnInstanceBackupDelete", nil) + if err := ib.GetRegionDriver().RequestDeleteInstanceBackup(ctx, ib, self); err != nil { + self.taskFailed(ctx, ib, jsonutils.NewString(err.Error())) + return + } +} + +func (self *InstanceBackupDeleteTask) OnKvmDiskBackupDelete( + ctx context.Context, isp *models.SInstanceBackup, data jsonutils.JSONObject) { + backupId, _ := self.Params.GetString("del_backup_id") + // detach backup and instance + isjp := new(models.SInstanceBackupJoint) + err := models.InstanceBackupJointManager.Query(). + Equals("instance_backup_id", isp.Id).Equals("disk_backup_id", backupId).First(isjp) + if err != nil { + self.taskFailed(ctx, isp, jsonutils.NewString(err.Error())) + return + } + isjp.SetModelManager(models.InstanceBackupJointManager, isjp) + err = isjp.Delete(ctx, self.UserCred) + if err != nil { + self.taskFailed(ctx, isp, jsonutils.NewString(err.Error())) + return + } + if err := isp.GetRegionDriver().RequestDeleteInstanceBackup(ctx, isp, self); err != nil { + self.taskFailed(ctx, isp, jsonutils.NewString(err.Error())) + return + } +} + +func (self *InstanceBackupDeleteTask) OnKvmBackupDeleteFailed( + ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + self.taskFailed(ctx, ib, data) +} + +func (self *InstanceBackupDeleteTask) OnInstanceBackupDelete(ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + self.taskSuccess(ctx, ib, data) + +} + +func (self *InstanceBackupDeleteTask) OnInstanceBackupDeleteFailed(ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + self.taskFailed(ctx, ib, data) +} diff --git a/pkg/compute/tasks/instance_backup_recovery_task.go b/pkg/compute/tasks/instance_backup_recovery_task.go new file mode 100644 index 0000000000..b6b7351091 --- /dev/null +++ b/pkg/compute/tasks/instance_backup_recovery_task.go @@ -0,0 +1,113 @@ +// 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 tasks + +import ( + "context" + "fmt" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" + compute_modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type InstanceBackupRecoveryTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(InstanceBackupRecoveryTask{}) +} + +func (self *InstanceBackupRecoveryTask) taskFailed(ctx context.Context, ib *models.SInstanceBackup, reason jsonutils.JSONObject) { + reasonStr, _ := reason.GetString() + ib.SetStatus(self.UserCred, compute.INSTANCE_BACKUP_STATUS_CREATE_FAILED, reasonStr) + logclient.AddActionLogWithStartable(self, ib, logclient.ACT_RECOVERY, reason, self.UserCred, false) + self.SetStageFailed(ctx, reason) +} + +func (self *InstanceBackupRecoveryTask) taskSuccess(ctx context.Context, ib *models.SInstanceBackup) { + ib.SetStatus(self.UserCred, compute.INSTANCE_BACKUP_STATUS_READY, "") + logclient.AddActionLogWithStartable(self, ib, logclient.ACT_RECOVERY, nil, self.UserCred, true) + self.SetStageComplete(ctx, nil) +} + +func (self *InstanceBackupRecoveryTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + ib := obj.(*models.SInstanceBackup) + serverName, _ := self.Params.GetString("server_name") + if serverName == "" { + serverName, _ = ib.ServerConfig.GetString("name") + } + sourceInput := &compute.ServerCreateInput{} + sourceInput.ServerConfigs = &compute.ServerConfigs{} + sourceInput.GenerateName = serverName + sourceInput.Description = fmt.Sprintf("recovery from instance backup %s", ib.GetName()) + sourceInput.InstanceBackupId = ib.GetId() + sourceInput.Hypervisor = compute.HYPERVISOR_KVM + taskHeader := self.GetTaskRequestHeader() + session := auth.GetSession(ctx, self.UserCred, "", "") + session.Header.Set(mcclient.TASK_NOTIFY_URL, taskHeader.Get(mcclient.TASK_NOTIFY_URL)) + session.Header.Set(mcclient.TASK_ID, taskHeader.Get(mcclient.TASK_ID)) + serverData, err := compute_modules.Servers.Create(session, jsonutils.Marshal(sourceInput)) + if err != nil { + self.taskFailed(ctx, ib, jsonutils.NewString(err.Error())) + return + } + guestId, _ := serverData.GetString("id") + params := jsonutils.NewDict() + params.Set("guest_id", jsonutils.NewString(guestId)) + self.SetStage("OnCreateGuest", params) +} + +func (self *InstanceBackupRecoveryTask) OnCreateGuest(ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + guestId, _ := self.Params.GetString("guest_id") + guest := models.GuestManager.FetchGuestById(guestId) + if guest == nil { + self.taskFailed(ctx, ib, jsonutils.NewString(fmt.Sprintf("no such guest %s", guest.GetId()))) + return + } + disks, err := guest.GetDisks() + if err != nil { + self.taskFailed(ctx, ib, jsonutils.NewString(err.Error())) + return + } + if len(disks) == 0 { + self.taskFailed(ctx, ib, jsonutils.NewString(fmt.Sprintf("no disks in guest %s?", guestId))) + return + } + sysDisk := &disks[0] + backups, err := ib.GetBackups() + if err != nil { + self.taskFailed(ctx, ib, jsonutils.NewString(fmt.Sprintf(err.Error()))) + return + } + db.Update(sysDisk, func() error { + sysDisk.TemplateId, _ = backups[0].DiskConfig.GetString("image_id") + sysDisk.SnapshotId, _ = backups[0].DiskConfig.GetString("snapshot_id") + return nil + }) + self.taskSuccess(ctx, ib) +} + +func (self *InstanceBackupRecoveryTask) OnCreateGuestFailed(ctx context.Context, ib *models.SInstanceBackup, data jsonutils.JSONObject) { + self.taskFailed(ctx, ib, data) +} From 342a220a2d18a69378be0073f73ed8c4ebfbfd27 Mon Sep 17 00:00:00 2001 From: rainzm Date: Wed, 19 Jan 2022 11:31:24 +0800 Subject: [PATCH 4/6] feat(climc): add climc command for instance/disk backup --- cmd/climc/shell/compute/backup.go | 43 ++++++ cmd/climc/shell/compute/servers.go | 14 ++ pkg/apis/compute/instance_backup.go | 46 +++++++ pkg/mcclient/modules/compute/mod_backup.go | 64 +++++++++ pkg/mcclient/options/compute/backup.go | 146 +++++++++++++++++++++ 5 files changed, 313 insertions(+) create mode 100644 cmd/climc/shell/compute/backup.go create mode 100644 pkg/apis/compute/instance_backup.go create mode 100644 pkg/mcclient/modules/compute/mod_backup.go create mode 100644 pkg/mcclient/options/compute/backup.go diff --git a/cmd/climc/shell/compute/backup.go b/cmd/climc/shell/compute/backup.go new file mode 100644 index 0000000000..3d64a2d50a --- /dev/null +++ b/cmd/climc/shell/compute/backup.go @@ -0,0 +1,43 @@ +// 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 compute + +import ( + "yunion.io/x/onecloud/cmd/climc/shell" + modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" + "yunion.io/x/onecloud/pkg/mcclient/options/compute" +) + +func init() { + bsCmd := shell.NewResourceCmd(&modules.BackupStorages) + bsCmd.List(&compute.BackupStorageListOptions{}) + bsCmd.Show(&compute.BackupStorageIdOptions{}) + bsCmd.Create(&compute.BackupStorageCreateOptions{}) + bsCmd.Delete(&compute.BackupStorageIdOptions{}) + + dbCmd := shell.NewResourceCmd(&modules.DiskBackups) + dbCmd.List(&compute.DiskBackupListOptions{}) + dbCmd.Show(&compute.DiskBackupIdOptions{}) + dbCmd.Delete(&compute.DiskBackupIdOptions{}) + dbCmd.Create(&compute.DiskBackupCreateOptions{}) + dbCmd.Perform("recovery", &compute.DiskBackupRecoveryOptions{}) + dbCmd.Perform("syncstatus", &compute.DiskBackupSyncstatusOptions{}) + + ibCmd := shell.NewResourceCmd(&modules.InstanceBackups) + ibCmd.List(&compute.InstanceBackupListOptions{}) + ibCmd.Show(&compute.InstanceBackupIdOptions{}) + ibCmd.Delete(&compute.InstanceBackupIdOptions{}) + ibCmd.Perform("recovery", &compute.InstanceBackupRecoveryOptions{}) +} diff --git a/cmd/climc/shell/compute/servers.go b/cmd/climc/shell/compute/servers.go index 7243987160..a29dae200c 100644 --- a/cmd/climc/shell/compute/servers.go +++ b/cmd/climc/shell/compute/servers.go @@ -742,6 +742,20 @@ func init() { printObject(result) return nil }) + type ServerCreateBackup struct { + ID string `help:"ID or name of VM" json:"-"` + BACKUP string `help:"Instance backup name" json:"name"` + BACKUPSTORAGEID string `help:"backup storage id" json:"backup_storage_id"` + } + R(&ServerCreateBackup{}, "instance-backup-create", "create instance backup", func(s *mcclient.ClientSession, opts *ServerCreateBackup) error { + params := jsonutils.Marshal(opts) + result, err := modules.Servers.PerformAction(s, opts.ID, "instance-backup", params) + if err != nil { + return err + } + printObject(result) + return nil + }) type ServerSnapshotAndClone struct { ID string `help:"ID or name of VM" json:"-"` diff --git a/pkg/apis/compute/instance_backup.go b/pkg/apis/compute/instance_backup.go new file mode 100644 index 0000000000..85999e5d20 --- /dev/null +++ b/pkg/apis/compute/instance_backup.go @@ -0,0 +1,46 @@ +package compute + +import "yunion.io/x/onecloud/pkg/apis" + +const ( + INSTANCE_BACKUP_STATUS_CREATING = "creating" + INSTANCE_BACKUP_STATUS_CREATE_FAILED = "create_failed" + INSTANCE_BACKUP_STATUS_DELETING = "deleting" + INSTANCE_BACKUP_STATUS_DELETE_FAILED = "delete_failed" + INSTANCE_BACKUP_STATUS_RECOVERY = "recovery" + INSTANCE_BACKUP_STATUS_RECOVERY_FAILED = "recovery_failed" + INSTANCE_BACKUP_STATUS_READY = "ready" +) + +type InstanceBackupListInput struct { + apis.VirtualResourceListInput + apis.MultiArchResourceBaseListInput + + ManagedResourceListInput + + ServerFilterListInput + + // 操作系统类型 + OsType []string `json:"os_type"` +} + +type InstanceBackupDetails struct { + apis.VirtualResourceDetails + ManagedResourceInfo + + // 云主机状态 + GuestStatus string `json:"guest_status"` + // 云主机名称 + Guest string `json:"guest"` + + // 存储类型 + BackupStorageName string `json:"backup_storage_name"` + + // 主机快照大小 + Size int `json:"size"` +} + +type InstanceBackupRecoveryInput struct { + // description: name of guest + Name string +} diff --git a/pkg/mcclient/modules/compute/mod_backup.go b/pkg/mcclient/modules/compute/mod_backup.go new file mode 100644 index 0000000000..21286bb97e --- /dev/null +++ b/pkg/mcclient/modules/compute/mod_backup.go @@ -0,0 +1,64 @@ +// 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 compute + +import ( + "yunion.io/x/onecloud/pkg/mcclient/modulebase" + "yunion.io/x/onecloud/pkg/mcclient/modules" +) + +type DiskBackupManager struct { + modulebase.ResourceManager +} + +type BackupStorageManager struct { + modulebase.ResourceManager +} + +type InstanceBackupManager struct { + modulebase.ResourceManager +} + +var ( + DiskBackups DiskBackupManager + BackupStorages BackupStorageManager + InstanceBackups InstanceBackupManager +) + +func init() { + DiskBackups = DiskBackupManager{modules.NewComputeManager( + "diskbackup", + "diskbackups", + []string{}, + []string{}, + )} + modules.RegisterCompute(&DiskBackups) + + BackupStorages = BackupStorageManager{modules.NewComputeManager( + "backupstorage", + "backupstorages", + []string{}, + []string{}, + )} + modules.RegisterCompute(&BackupStorages) + + InstanceBackups = InstanceBackupManager{modules.NewComputeManager( + "instancebackup", + "instancebackups", + []string{}, + []string{}, + )} + modules.RegisterCompute(&InstanceBackups) +} diff --git a/pkg/mcclient/options/compute/backup.go b/pkg/mcclient/options/compute/backup.go new file mode 100644 index 0000000000..7ff1ef96e9 --- /dev/null +++ b/pkg/mcclient/options/compute/backup.go @@ -0,0 +1,146 @@ +// 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 compute + +import ( + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/mcclient/options" +) + +type DiskBackupListOptions struct { + options.BaseListOptions + DiskId string `help:"disk id" json:"disk_id"` + BackupStorageId string `help:"backup storage id" json:"backup_storage_id"` + IsInstanceBackup *bool `help:"if part of instance backup" json:"is_instance_backup"` +} + +func (opts *DiskBackupListOptions) Params() (jsonutils.JSONObject, error) { + return options.ListStructToParams(opts) +} + +type DiskBackupIdOptions struct { + ID string `help:"disk backup id" json:"-"` +} + +func (opts *DiskBackupIdOptions) GetId() string { + return opts.ID +} + +func (opts *DiskBackupIdOptions) Params() (jsonutils.JSONObject, error) { + return nil, nil +} + +type DiskBackupCreateOptions struct { + options.BaseCreateOptions + DISKID string `help:"disk id" json:"disk_id"` + BACKUPSTORAGEID string `help:"back storage id" json:"backup_storage_id"` +} + +func (opts *DiskBackupCreateOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(opts), nil +} + +type DiskBackupRecoveryOptions struct { + DiskBackupIdOptions + Name string `help:"disk name" json:"name"` +} + +func (opt *DiskBackupRecoveryOptions) GetId() string { + return opt.ID +} + +func (opt *DiskBackupRecoveryOptions) Params() (jsonutils.JSONObject, error) { + params := jsonutils.NewDict() + params.Set("name", jsonutils.NewString(opt.Name)) + return params, nil +} + +type DiskBackupSyncstatusOptions struct { + DiskBackupIdOptions +} + +func (opt *DiskBackupSyncstatusOptions) GetId() string { + return opt.ID +} + +func (opt *DiskBackupSyncstatusOptions) Params() (jsonutils.JSONObject, error) { + return nil, nil +} + +type BackupStorageListOptions struct { + options.BaseListOptions +} + +func (opts *BackupStorageListOptions) Params() (jsonutils.JSONObject, error) { + return options.ListStructToParams(opts) +} + +type BackupStorageIdOptions struct { + ID string `help:"backup storage id"` +} + +func (opts *BackupStorageIdOptions) GetId() string { + return opts.ID +} + +func (opts *BackupStorageIdOptions) Params() (jsonutils.JSONObject, error) { + return nil, nil +} + +type BackupStorageCreateOptions struct { + options.BaseCreateOptions + StorageType string `help:"storage type" choices:"nfs"` + NfsHost string `help:"nfs host, required when storage_type is nfs"` + NfsSharedDir string `help:"nfs shared dir, required when storage_type is nfs" ` + CapacityMb int `help:"capacity, unit mb"` +} + +func (opts *BackupStorageCreateOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(opts), nil +} + +type InstanceBackupListOptions struct { + options.BaseListOptions +} + +func (opts *InstanceBackupListOptions) Params() (jsonutils.JSONObject, error) { + return options.ListStructToParams(opts) +} + +type InstanceBackupIdOptions struct { + ID string `help:"instance backup id"` +} + +func (opts *InstanceBackupIdOptions) GetId() string { + return opts.ID +} + +func (opts *InstanceBackupIdOptions) Params() (jsonutils.JSONObject, error) { + return nil, nil +} + +type InstanceBackupRecoveryOptions struct { + DiskBackupIdOptions + Name string `help:"server name" json:"name"` +} + +func (opts *InstanceBackupRecoveryOptions) GetId() string { + return opts.ID +} + +func (opts *InstanceBackupRecoveryOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(opts), nil +} From d8611811b5335707e444a63c80118ed5ada6a5e4 Mon Sep 17 00:00:00 2001 From: rainzm Date: Wed, 19 Jan 2022 14:48:17 +0800 Subject: [PATCH 5/6] fix(region): set status creating for instance backup when creating --- pkg/compute/models/instance_backup.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/compute/models/instance_backup.go b/pkg/compute/models/instance_backup.go index 5e8f702336..5e99cf7522 100644 --- a/pkg/compute/models/instance_backup.go +++ b/pkg/compute/models/instance_backup.go @@ -224,6 +224,7 @@ func (manager *SInstanceBackupManager) FetchCustomizeColumns( } func (self *SInstanceBackup) StartCreateInstanceBackupTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error { + self.SetStatus(userCred, api.INSTANCE_BACKUP_STATUS_CREATING, "") if task, err := taskman.TaskManager.NewTask(ctx, "InstanceBackupCreateTask", self, userCred, nil, parentTaskId, "", nil); err != nil { return err } else { From 745d8123d4a6f2599a26f91f24f9a1b90cbfc7ad Mon Sep 17 00:00:00 2001 From: rainzm Date: Sat, 22 Jan 2022 15:27:33 +0800 Subject: [PATCH 6/6] fix(region): optimize codes 1. do not ignore err 2. replace jsonutils.JSONObject with specific structure 3. delete redundant code 4. fix edge problems --- pkg/compute/models/backup.go | 46 +++++++++++++------ pkg/compute/models/backup_storage.go | 32 +------------ pkg/compute/models/hosts.go | 4 +- pkg/compute/regiondrivers/kvm.go | 8 ++-- pkg/compute/tasks/disk_backup_create_task.go | 7 ++- pkg/compute/tasks/disk_backup_delete_task.go | 7 ++- .../tasks/disk_backup_recovery_task.go | 13 ++---- .../tasks/disk_backup_syncstatus_task.go | 7 ++- .../tasks/instance_backup_recovery_task.go | 4 +- 9 files changed, 64 insertions(+), 64 deletions(-) diff --git a/pkg/compute/models/backup.go b/pkg/compute/models/backup.go index 22f715a891..72b7be79f1 100644 --- a/pkg/compute/models/backup.go +++ b/pkg/compute/models/backup.go @@ -16,11 +16,13 @@ package models import ( "context" + "reflect" "time" "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/gotypes" "yunion.io/x/sqlchemy" api "yunion.io/x/onecloud/pkg/apis/compute" @@ -56,13 +58,16 @@ type SDiskBackup struct { DiskSizeMb int `nullable:"false" list:"user" create:"optional"` DiskType string `width:"32" charset:"ascii" nullable:"true" list:"user" create:"optional"` // 操作系统类型 - OsType string `width:"32" charset:"ascii" nullable:"true" list:"user" create:"optional"` - DiskConfig jsonutils.JSONObject + OsType string `width:"32" charset:"ascii" nullable:"true" list:"user" create:"optional"` + DiskConfig *SBackupDiskConfig `ignore:"true"` } var DiskBackupManager *SDiskBackupManager func init() { + gotypes.RegisterSerializable(reflect.TypeOf(&SBackupDiskConfig{}), func() gotypes.ISerializable { + return &SBackupDiskConfig{} + }) DiskBackupManager = &SDiskBackupManager{ SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager( SDiskBackup{}, @@ -74,6 +79,19 @@ func init() { DiskBackupManager.SetVirtualObject(DiskBackupManager) } +type SBackupDiskConfig struct { + api.DiskConfig + Name string +} + +func (dc *SBackupDiskConfig) String() string { + return jsonutils.Marshal(dc).String() +} + +func (dc *SBackupDiskConfig) IsZero() bool { + return dc == nil +} + func (dm *SDiskBackupManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.DiskBackupListInput) (*sqlchemy.SQuery, error) { var err error q, err = dm.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, input.VirtualResourceListInput) @@ -179,12 +197,12 @@ func (db *SDiskBackup) GetBackupStorage() (*SBackupStorage, error) { return ibs.(*SBackupStorage), nil } -func (db *SDiskBackup) GetRegionDriver() IRegionDriver { - cloudRegion, _ := db.GetRegion() - if cloudRegion != nil { - return cloudRegion.GetDriver() +func (db *SDiskBackup) GetRegionDriver() (IRegionDriver, error) { + cloudRegion, err := db.GetRegion() + if err != nil { + return nil, errors.Wrap(err, "db.GetRegion") } - return nil + return cloudRegion.GetDriver(), nil } func (dm *SDiskBackupManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.DiskBackupCreateInput) (api.DiskBackupCreateInput, error) { @@ -231,9 +249,10 @@ func (db *SDiskBackup) CustomizeCreate(ctx context.Context, userCred mcclient.To return errors.Wrap(err, "DiskManager.FetchById") } disk := diskObj.(*SDisk) - diskConfig := jsonutils.Marshal(disk.ToDiskConfig()).(*jsonutils.JSONDict) - diskConfig.Set("name", jsonutils.NewString(disk.GetName())) - db.DiskConfig = diskConfig + db.DiskConfig = &SBackupDiskConfig{ + DiskConfig: *disk.ToDiskConfig(), + Name: disk.GetName(), + } db.DiskType = disk.DiskType db.DiskSizeMb = disk.DiskSize db.OsArch = disk.OsArch @@ -376,9 +395,10 @@ func (manager *SDiskBackupManager) CreateBackup(ctx context.Context, owner mccli backup.ProjectId = owner.GetProjectId() backup.DomainId = owner.GetProjectDomainId() backup.DiskId = disk.Id - diskConfig := jsonutils.Marshal(disk.ToDiskConfig()).(*jsonutils.JSONDict) - diskConfig.Set("name", jsonutils.NewString(disk.GetName())) - backup.DiskConfig = diskConfig + backup.DiskConfig = &SBackupDiskConfig{ + DiskConfig: *disk.ToDiskConfig(), + Name: disk.GetName(), + } backup.DiskType = disk.DiskType backup.DiskSizeMb = disk.DiskSize backup.OsArch = disk.OsArch diff --git a/pkg/compute/models/backup_storage.go b/pkg/compute/models/backup_storage.go index 6a84f18e3e..b5743e1687 100644 --- a/pkg/compute/models/backup_storage.go +++ b/pkg/compute/models/backup_storage.go @@ -19,9 +19,7 @@ import ( "reflect" "yunion.io/x/jsonutils" - "yunion.io/x/pkg/errors" "yunion.io/x/pkg/gotypes" - "yunion.io/x/pkg/tristate" "yunion.io/x/pkg/utils" "yunion.io/x/sqlchemy" @@ -109,7 +107,7 @@ func (bs *SBackupStorage) CustomizeCreate(ctx context.Context, userCred mcclient } func (bs *SBackupStorage) BackupCount() (int, error) { - return 0, nil + return DiskBackupManager.Query().Equals("backup_storage_id", bs.GetId()).CountWithError() } func (bs *SBackupStorage) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error { @@ -127,34 +125,6 @@ func (bs *SBackupStorage) PostCreate(ctx context.Context, userCred mcclient.Toke bs.SEnabledStatusInfrasResourceBase.PostCreate(ctx, userCred, ownerId, query, data) } -func (bs *SBackupStorage) PerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { - if bs.Enabled.IsFalse() { - _, err := db.Update(bs, func() error { - bs.Enabled = tristate.True - return nil - }) - if err != nil { - return nil, errors.Wrap(err, "unable to enable") - } - db.OpsLog.LogEvent(bs, db.ACT_ENABLE, "", userCred) - } - return nil, nil -} - -func (bs *SBackupStorage) PerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { - if bs.Enabled.IsTrue() { - _, err := db.Update(bs, func() error { - bs.Enabled = tristate.False - return nil - }) - if err != nil { - return nil, errors.Wrap(err, "unable to disable") - } - db.OpsLog.LogEvent(bs, db.ACT_DISABLE, "", userCred) - } - return nil, nil -} - func (bs *SBackupStorage) getMoreDetails(ctx context.Context, out api.BackupStorageDetails) api.BackupStorageDetails { out.NfsHost = bs.AccessInfo.NfsHost out.NfsSharedDir = bs.AccessInfo.NfsSharedDir diff --git a/pkg/compute/models/hosts.go b/pkg/compute/models/hosts.go index bf7026abdd..6252ea7a82 100644 --- a/pkg/compute/models/hosts.go +++ b/pkg/compute/models/hosts.go @@ -1383,8 +1383,8 @@ func (self *SHost) GetMasterHostwire() *SHostwire { return &hw } -func (self *SHostManager) GetEnabledHost() (*SHost, error) { - hostq := HostManager.Query().IsTrue("enabled").Equals("host_status", api.HOST_ONLINE) +func (self *SHostManager) GetEnabledKvmHost() (*SHost, error) { + hostq := HostManager.Query().IsTrue("enabled").Equals("host_status", api.HOST_ONLINE).In("host_type", []string{api.HOST_TYPE_HYPERVISOR, api.HOST_TYPE_KVM}) host := SHost{} err := hostq.First(&host) if err != nil { diff --git a/pkg/compute/regiondrivers/kvm.go b/pkg/compute/regiondrivers/kvm.go index bd91f9d492..8845c673b3 100644 --- a/pkg/compute/regiondrivers/kvm.go +++ b/pkg/compute/regiondrivers/kvm.go @@ -1303,9 +1303,9 @@ func (self *SKVMRegionDriver) RequestSyncDiskBackupStatus(ctx context.Context, u host = storage.GetMasterHost() } if host == nil { - host, err = models.HostManager.GetEnabledHost() + host, err = models.HostManager.GetEnabledKvmHost() if err != nil { - return nil, errors.Wrap(err, "unable to GetEnabledHost") + return nil, errors.Wrap(err, "unable to GetEnabledKvmHost") } } log.Infof("host: %s, ManagerUri: %s", host.GetId(), host.ManagerUri) @@ -1527,9 +1527,9 @@ func (self *SKVMRegionDriver) RequestDeleteBackup(ctx context.Context, backup *m host = storage.GetMasterHost() } if host == nil { - host, err = models.HostManager.GetEnabledHost() + host, err = models.HostManager.GetEnabledKvmHost() if err != nil { - return errors.Wrap(err, "unable to GetEnabledHost") + return errors.Wrap(err, "unable to GetEnabledKvmHost") } } url := fmt.Sprintf("%s/storages/delete-backup", host.ManagerUri) diff --git a/pkg/compute/tasks/disk_backup_create_task.go b/pkg/compute/tasks/disk_backup_create_task.go index 0c1af70890..718f3504ed 100644 --- a/pkg/compute/tasks/disk_backup_create_task.go +++ b/pkg/compute/tasks/disk_backup_create_task.go @@ -100,7 +100,12 @@ func (self *DiskBackupCreateTask) OnSnapshot(ctx context.Context, backup *models } backup.SetStatus(self.UserCred, api.BACKUP_STATUS_SAVING, "") self.SetStage("OnSave", nil) - if err := backup.GetRegionDriver().RequestCreateBackup(ctx, backup, snapshotId, self); err != nil { + rd, err := backup.GetRegionDriver() + if err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error()), api.BACKUP_STATUS_SAVE_FAILED) + return + } + if err := rd.RequestCreateBackup(ctx, backup, snapshotId, self); err != nil { self.taskFailed(ctx, backup, jsonutils.NewString(err.Error()), api.BACKUP_STATUS_SAVE_FAILED) } } diff --git a/pkg/compute/tasks/disk_backup_delete_task.go b/pkg/compute/tasks/disk_backup_delete_task.go index 5f2fbce7b5..d5ab5d75e4 100644 --- a/pkg/compute/tasks/disk_backup_delete_task.go +++ b/pkg/compute/tasks/disk_backup_delete_task.go @@ -49,7 +49,12 @@ func (self *DiskBackupDeleteTask) taskSuccess(ctx context.Context, backup *model func (self *DiskBackupDeleteTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { backup := obj.(*models.SDiskBackup) self.SetStage("OnDelete", nil) - if err := backup.GetRegionDriver().RequestDeleteBackup(ctx, backup, self); err != nil { + rd, err := backup.GetRegionDriver() + if err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error())) + return + } + if err := rd.RequestDeleteBackup(ctx, backup, self); err != nil { self.taskFailed(ctx, backup, jsonutils.NewString(err.Error())) } } diff --git a/pkg/compute/tasks/disk_backup_recovery_task.go b/pkg/compute/tasks/disk_backup_recovery_task.go index 5e7b7f41de..3bee5b11d2 100644 --- a/pkg/compute/tasks/disk_backup_recovery_task.go +++ b/pkg/compute/tasks/disk_backup_recovery_task.go @@ -42,14 +42,9 @@ func (self *DiskBackupRecoveryTask) OnInit(ctx context.Context, obj db.IStandalo backup := obj.(*models.SDiskBackup) diskName, _ := self.Params.GetString("disk_name") if diskName == "" { - diskName, _ = backup.DiskConfig.GetString("name") - } - diskConfig := &api.DiskConfig{} - err := backup.DiskConfig.Unmarshal(diskConfig) - if err != nil { - self.taskFaild(ctx, backup, jsonutils.NewString(err.Error())) - return + diskName = backup.DiskConfig.Name } + diskConfig := &backup.DiskConfig.DiskConfig diskConfig.ImageId = "" diskConfig.SnapshotId = "" diskConfig.BackupId = backup.GetId() @@ -82,8 +77,8 @@ func (self *DiskBackupRecoveryTask) OnCreateDisk(ctx context.Context, backup *mo self.taskFaild(ctx, backup, jsonutils.NewString(fmt.Sprintf("disk %s disappeared", diskId))) return } - imageId, _ := backup.DiskConfig.GetString("image_id") - snapshotId, _ := backup.DiskConfig.GetString("snapshot_id") + imageId := backup.DiskConfig.ImageId + snapshotId := backup.DiskConfig.SnapshotId db.Update(disk, func() error { disk.TemplateId = imageId disk.SnapshotId = snapshotId diff --git a/pkg/compute/tasks/disk_backup_syncstatus_task.go b/pkg/compute/tasks/disk_backup_syncstatus_task.go index 80002ade83..691144b0e2 100644 --- a/pkg/compute/tasks/disk_backup_syncstatus_task.go +++ b/pkg/compute/tasks/disk_backup_syncstatus_task.go @@ -42,7 +42,12 @@ func (self *DiskBackupSyncstatusTask) OnInit(ctx context.Context, obj db.IStanda backup := obj.(*models.SDiskBackup) self.SetStage("OnDiskBackupSyncStatus", nil) - err := backup.GetRegionDriver().RequestSyncDiskBackupStatus(ctx, self.GetUserCred(), backup, self) + rd, err := backup.GetRegionDriver() + if err != nil { + self.taskFailed(ctx, backup, jsonutils.NewString(err.Error())) + return + } + err = rd.RequestSyncDiskBackupStatus(ctx, self.GetUserCred(), backup, self) if err != nil { self.taskFailed(ctx, backup, jsonutils.NewString(err.Error())) return diff --git a/pkg/compute/tasks/instance_backup_recovery_task.go b/pkg/compute/tasks/instance_backup_recovery_task.go index b6b7351091..a71cee57b7 100644 --- a/pkg/compute/tasks/instance_backup_recovery_task.go +++ b/pkg/compute/tasks/instance_backup_recovery_task.go @@ -101,8 +101,8 @@ func (self *InstanceBackupRecoveryTask) OnCreateGuest(ctx context.Context, ib *m return } db.Update(sysDisk, func() error { - sysDisk.TemplateId, _ = backups[0].DiskConfig.GetString("image_id") - sysDisk.SnapshotId, _ = backups[0].DiskConfig.GetString("snapshot_id") + sysDisk.TemplateId = backups[0].DiskConfig.ImageId + sysDisk.SnapshotId = backups[0].DiskConfig.SnapshotId return nil }) self.taskSuccess(ctx, ib)