diff --git a/cmd/climc/shell/compute/servers.go b/cmd/climc/shell/compute/servers.go index 5e73293411..5c97c021e6 100644 --- a/cmd/climc/shell/compute/servers.go +++ b/cmd/climc/shell/compute/servers.go @@ -100,6 +100,7 @@ func init() { cmd.Perform("set-sshport", &options.ServerSetSshportOptions{}) cmd.Perform("have-agent", &options.ServerHaveAgentOptions{}) cmd.Perform("change-disk-storage", &options.ServerChangeDiskStorageOptions{}) + cmd.Perform("change-storage", &options.ServerChangeStorageOptions{}) cmd.PerformClass("batch-user-metadata", &options.ServerBatchMetadataOptions{}) cmd.PerformClass("batch-set-user-metadata", &options.ServerBatchMetadataOptions{}) cmd.Perform("user-metadata", &baseoptions.ResourceMetadataOptions{}) diff --git a/pkg/apis/compute/guests.go b/pkg/apis/compute/guests.go index 9a97fcb17d..1a26a66dd1 100644 --- a/pkg/apis/compute/guests.go +++ b/pkg/apis/compute/guests.go @@ -847,6 +847,17 @@ type ServerSetBootIndexInput struct { Cdroms map[string]int8 `json:"cdroms"` } +type ServerChangeStorageInput struct { + TargetStorageId string `json:"target_storage_id"` + KeepOriginDisk bool `json:"keep_origin_disk"` +} + +type ServerChangeStorageInternalInput struct { + ServerChangeStorageInput + Disks []string `json:"disks"` + GuestRunning bool `json:"guest_running"` +} + type ServerChangeDiskStorageInput struct { DiskId string `json:"disk_id"` TargetStorageId string `json:"target_storage_id"` @@ -858,7 +869,7 @@ type ServerChangeDiskStorageInternalInput struct { StorageId string `json:"storage_id"` TargetDiskId string `json:"target_disk_id"` DiskFormat string `json:"disk_format"` - GuestRunning bool `josn:"guest_running"` + GuestRunning bool `json:"guest_running"` TargetDiskDesc *GuestdiskJsonDesc `json:"target_disk_desc"` } diff --git a/pkg/compute/guestdrivers/base.go b/pkg/compute/guestdrivers/base.go index 2d49d187fb..ac4dd20661 100644 --- a/pkg/compute/guestdrivers/base.go +++ b/pkg/compute/guestdrivers/base.go @@ -454,7 +454,7 @@ func (self *SBaseGuestDriver) ValidateDetachNetwork(ctx context.Context, userCre return nil } -func (self *SBaseGuestDriver) ValidateChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, input *api.ServerChangeDiskStorageInput) error { +func (self *SBaseGuestDriver) ValidateChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, targetStorageId string) error { return cloudprovider.ErrNotImplemented } diff --git a/pkg/compute/guestdrivers/kvm.go b/pkg/compute/guestdrivers/kvm.go index 980edea32b..3aecab4b1b 100644 --- a/pkg/compute/guestdrivers/kvm.go +++ b/pkg/compute/guestdrivers/kvm.go @@ -847,7 +847,7 @@ func (self *SKVMGuestDriver) ValidateDetachNetwork(ctx context.Context, userCred return nil } -func (self *SKVMGuestDriver) ValidateChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, input *api.ServerChangeDiskStorageInput) error { +func (self *SKVMGuestDriver) ValidateChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, targetStorageId string) error { if !utils.IsInStringArray(guest.Status, []string{api.VM_READY, api.VM_RUNNING, api.VM_BLOCK_STREAM, api.VM_DISK_CHANGE_STORAGE}) { return httperrors.NewBadRequestError("Cannot change disk storage in status %s", guest.Status) } @@ -865,12 +865,12 @@ func (self *SKVMGuestDriver) ValidateChangeDiskStorage(ctx context.Context, user attachedStorages := host.GetAttachedEnabledHostStorages(nil) foundStorage := false for _, storage := range attachedStorages { - if storage.GetId() == input.TargetStorageId { + if storage.GetId() == targetStorageId { foundStorage = true } } if !foundStorage { - return httperrors.NewBadRequestError("Storage %s not attached or enabled on host %s", input.TargetStorageId, host.GetName()) + return httperrors.NewBadRequestError("Storage %s not attached or enabled on host %s", targetStorageId, host.GetName()) } return nil } diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index d8166b2d3f..778103fb7a 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -3476,11 +3476,11 @@ func (self *SGuest) PerformBlockMirrorReady(ctx context.Context, userCred mcclie if disk == nil { return nil, httperrors.NewNotFoundError("disk %s not found", diskId) } - - taskId := disk.GetMetadata(ctx, api.DISK_CLONE_TASK_ID, userCred) - log.Infof("task_id %s", taskId) - if err := self.startSwitchToClonedDisk(ctx, userCred, taskId); err != nil { - return nil, errors.Wrap(err, "startSwitchToClonedDisk") + if taskId := disk.GetMetadata(ctx, api.DISK_CLONE_TASK_ID, userCred); len(taskId) > 0 { + log.Infof("task_id %s", taskId) + if err := self.startSwitchToClonedDisk(ctx, userCred, taskId); err != nil { + return nil, errors.Wrap(err, "startSwitchToClonedDisk") + } } } return nil, nil @@ -5456,6 +5456,58 @@ func (self *SGuest) PerformListForward(ctx context.Context, userCred mcclient.To return resp.JSON(), nil } +func (self *SGuest) PerformChangeStorage(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input *api.ServerChangeStorageInput) (*api.ServerChangeStorageInput, error) { + // validate input + if input.TargetStorageId == "" { + return nil, httperrors.NewNotEmptyError("Storage id is empty") + } + + // validate storage + storageObj, err := StorageManager.FetchByIdOrName(userCred, input.TargetStorageId) + if err != nil { + return nil, errors.Wrapf(err, "Found storage by %s", input.TargetStorageId) + } + storage := storageObj.(*SStorage) + input.TargetStorageId = storage.GetId() + + // validate disk + disks, err := self.GetDisks() + if err != nil { + return nil, errors.Wrapf(err, "Get server %s disks", self.GetName()) + } + var changeDisks = []string{} + for _, disk := range disks { + if disk.StorageId != input.TargetStorageId { + changeDisks = append(changeDisks, disk.Id) + } + } + + // driver validate + drv := self.GetDriver() + if err := drv.ValidateChangeDiskStorage(ctx, userCred, self, input.TargetStorageId); err != nil { + return nil, err + } + return nil, self.StartGuestChangeStorageTask(ctx, userCred, input, changeDisks) +} + +func (self *SGuest) StartGuestChangeStorageTask(ctx context.Context, userCred mcclient.TokenCredential, input *api.ServerChangeStorageInput, disks []string) error { + params := api.ServerChangeStorageInternalInput{ + ServerChangeStorageInput: *input, + Disks: disks, + GuestRunning: self.Status == api.VM_RUNNING, + } + reason := fmt.Sprintf("Change guest disks storage to %s", input.TargetStorageId) + self.SetStatus(userCred, api.VM_DISK_CHANGE_STORAGE, reason) + if task, err := taskman.TaskManager.NewTask( + ctx, "GuestChangeDisksStorageTask", self, userCred, jsonutils.Marshal(params).(*jsonutils.JSONDict), + "", "", nil); err != nil { + return err + } else { + task.ScheduleRun(nil) + } + return nil +} + func (self *SGuest) PerformChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input *api.ServerChangeDiskStorageInput) (*api.ServerChangeDiskStorageInput, error) { // validate input if input.DiskId == "" { @@ -5492,7 +5544,7 @@ func (self *SGuest) PerformChangeDiskStorage(ctx context.Context, userCred mccli // driver validate drv := self.GetDriver() - if err := drv.ValidateChangeDiskStorage(ctx, userCred, self, input); err != nil { + if err := drv.ValidateChangeDiskStorage(ctx, userCred, self, input.TargetStorageId); err != nil { return nil, err } @@ -5505,7 +5557,7 @@ func (self *SGuest) PerformChangeDiskStorage(ctx context.Context, userCred mccli DiskType: srcDisk.DiskType, } - targetDisk, err := self.createDiskOnStorage(ctx, userCred, storage, diskConf, nil, true, true) + targetDisk, err := self.CreateDiskOnStorage(ctx, userCred, storage, diskConf, nil, true, true) if err != nil { return nil, errors.Wrapf(err, "Create target disk on storage %s", storage.GetName()) } @@ -5523,7 +5575,7 @@ func (self *SGuest) PerformChangeDiskStorage(ctx context.Context, userCred mccli func (self *SGuest) StartChangeDiskStorageTask(ctx context.Context, userCred mcclient.TokenCredential, input *api.ServerChangeDiskStorageInternalInput, parentTaskId string) error { reason := fmt.Sprintf("Change disk %s to storage %s", input.DiskId, input.TargetStorageId) self.SetStatus(userCred, api.VM_DISK_CHANGE_STORAGE, reason) - return self.GetDriver().StartChangeDiskStorageTask(self, ctx, userCred, input, "") + return self.GetDriver().StartChangeDiskStorageTask(self, ctx, userCred, input, parentTaskId) } func (self *SGuest) startSwitchToClonedDisk(ctx context.Context, userCred mcclient.TokenCredential, taskId string) error { diff --git a/pkg/compute/models/guestdrivers.go b/pkg/compute/models/guestdrivers.go index 1ac95c169a..a36427623b 100644 --- a/pkg/compute/models/guestdrivers.go +++ b/pkg/compute/models/guestdrivers.go @@ -218,7 +218,7 @@ type IGuestDriver interface { RequestListForward(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, req *guestdriver_types.ListForwardRequest) (*guestdriver_types.ListForwardResponse, error) RequestCloseForward(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, req *guestdriver_types.CloseForwardRequest) (*guestdriver_types.CloseForwardResponse, error) - ValidateChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, input *api.ServerChangeDiskStorageInput) error + ValidateChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, targetStorageId string) error StartChangeDiskStorageTask(guest *SGuest, ctx context.Context, userCred mcclient.TokenCredential, params *api.ServerChangeDiskStorageInternalInput, parentTaskId string) error RequestChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, input *api.ServerChangeDiskStorageInternalInput, task taskman.ITask) error RequestSwitchToTargetStorageDisk(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, input *api.ServerChangeDiskStorageInternalInput, task taskman.ITask) error diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index b56446600c..5ed0ac7038 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -4041,7 +4041,7 @@ func (self *SGuest) CreateDisksOnHost( return nil } -func (self *SGuest) createDiskOnStorage(ctx context.Context, userCred mcclient.TokenCredential, storage *SStorage, +func (self *SGuest) CreateDiskOnStorage(ctx context.Context, userCred mcclient.TokenCredential, storage *SStorage, diskConfig *api.DiskConfig, pendingUsage quotas.IQuota, inheritBilling bool, isWithServerCreate bool) (*SDisk, error) { lockman.LockObject(ctx, storage) defer lockman.ReleaseObject(ctx, storage) @@ -4128,7 +4128,7 @@ func (self *SGuest) createDiskOnHost( return nil, fmt.Errorf("No storage on %s to create disk for %s", host.GetName(), diskConfig.Backend) } log.Debugf("Choose storage %s:%s for disk %#v", storage.Name, storage.Id, diskConfig) - disk, err := self.createDiskOnStorage(ctx, userCred, storage, diskConfig, pendingUsage, inheritBilling, isWithServerCreate) + disk, err := self.CreateDiskOnStorage(ctx, userCred, storage, diskConfig, pendingUsage, inheritBilling, isWithServerCreate) if err != nil { return nil, err } diff --git a/pkg/compute/tasks/guest_change_disk_storage_task.go b/pkg/compute/tasks/guest_change_disk_storage_task.go index 8ff6d2f260..8008d3fd34 100644 --- a/pkg/compute/tasks/guest_change_disk_storage_task.go +++ b/pkg/compute/tasks/guest_change_disk_storage_task.go @@ -32,6 +32,7 @@ import ( func init() { taskman.RegisterTask(GuestChangeDiskStorageTask{}) + taskman.RegisterTask(GuestChangeDisksStorageTask{}) } type GuestChangeDiskStorageTask struct { @@ -100,6 +101,7 @@ func (t *GuestChangeDiskStorageTask) ChangeDiskStorage(ctx context.Context, gues return } + log.Infof("ChangeDiskStorage guest running is %v", input.GuestRunning) if input.GuestRunning { t.SetStage("OnDiskLiveChangeStorageReady", nil) } else { @@ -292,6 +294,10 @@ func (t *GuestChangeDiskStorageTask) attachTargetDisk(ctx context.Context, guest } func (t *GuestChangeDiskStorageTask) OnTargetDiskAttachComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + if t.HasParentTask() { + t.TaskComplete(ctx, guest, data) + return + } t.SetStage("OnGuestSyncStatus", nil) if err := guest.StartSyncstatus(ctx, t.UserCred, t.Id); err != nil { t.TaskFailed(ctx, guest, jsonutils.NewString(err.Error())) @@ -320,3 +326,107 @@ func (t *GuestChangeDiskStorageTask) TaskFailed(ctx context.Context, guest *mode logclient.AddActionLogWithStartable(t, guest, logclient.ACT_DISK_CHANGE_STORAGE, reason, t.GetUserCred(), false) t.SetStageFailed(ctx, reason) } + +// --------------------- GuestChangeDisksStorageTask ---------------------------- + +type GuestChangeDisksStorageTask struct { + SGuestBaseTask +} + +func (t *GuestChangeDisksStorageTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + guest := obj.(*models.SGuest) + t.ChangeDiskStorage(ctx, guest, nil) +} + +func (t *GuestChangeDisksStorageTask) GetInputParams() (*api.ServerChangeStorageInternalInput, error) { + input := new(api.ServerChangeStorageInternalInput) + err := t.GetParams().Unmarshal(input) + return input, err +} + +func (t *GuestChangeDisksStorageTask) TaskFailed(ctx context.Context, guest *models.SGuest, reason jsonutils.JSONObject) { + guest.SetStatus(t.GetUserCred(), api.VM_DISK_CHANGE_STORAGE_FAIL, reason.String()) + logclient.AddActionLogWithStartable(t, guest, logclient.ACT_DISK_CHANGE_STORAGE, reason, t.GetUserCred(), false) + t.SetStageFailed(ctx, reason) +} + +func (t *GuestChangeDisksStorageTask) TaskComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + logclient.AddActionLogWithStartable(t, guest, logclient.ACT_DISK_CHANGE_STORAGE, nil, t.GetUserCred(), true) + t.SetStageComplete(ctx, nil) +} + +func (t *GuestChangeDisksStorageTask) OnDiskChangeStorageComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + t.SetStage("OnGuestSyncStatus", nil) + if err := guest.StartSyncstatus(ctx, t.UserCred, t.Id); err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(err.Error())) + } +} + +func (t *GuestChangeDisksStorageTask) OnGuestSyncStatus(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + t.TaskComplete(ctx, guest, nil) +} + +func (t *GuestChangeDisksStorageTask) OnGuestSyncStatusFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + t.TaskFailed(ctx, guest, data) +} + +func (t *GuestChangeDisksStorageTask) ChangeDiskStorage(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + input, err := t.GetInputParams() + if err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(err.Error())) + return + } + if len(input.Disks) == 0 { + t.OnDiskChangeStorageComplete(ctx, guest, nil) + return + } + + t.CreateTargetDisk(ctx, guest, input) +} + +func (t *GuestChangeDisksStorageTask) ChangeDiskStorageFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + t.TaskFailed(ctx, guest, data) +} + +func (t *GuestChangeDisksStorageTask) CreateTargetDisk(ctx context.Context, guest *models.SGuest, input *api.ServerChangeStorageInternalInput) { + storage := models.StorageManager.FetchStorageById(input.TargetStorageId) + srcDisk := models.DiskManager.FetchDiskById(input.Disks[0]) + + input.Disks = input.Disks[1:] + t.Params.Set("disks", jsonutils.Marshal(input.Disks)) + if err := t.SetStage("ChangeDiskStorage", nil); err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(err.Error())) + return + } + + // create a disk on target storage from source disk + diskConf := &api.DiskConfig{ + Index: -1, + ImageId: srcDisk.TemplateId, + SizeMb: srcDisk.DiskSize, + Fs: srcDisk.FsFormat, + DiskType: srcDisk.DiskType, + } + + targetDisk, err := guest.CreateDiskOnStorage(ctx, t.UserCred, storage, diskConf, nil, true, true) + if err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("Create target disk on storage %s: %s", storage.GetName(), err))) + return + } + + internalInput := &api.ServerChangeDiskStorageInternalInput{ + ServerChangeDiskStorageInput: api.ServerChangeDiskStorageInput{ + DiskId: srcDisk.Id, + TargetStorageId: storage.Id, + KeepOriginDisk: input.KeepOriginDisk, + }, + StorageId: srcDisk.StorageId, + TargetDiskId: targetDisk.GetId(), + GuestRunning: input.GuestRunning, + } + + if err := guest.StartChangeDiskStorageTask(ctx, t.UserCred, internalInput, t.Id); err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(err.Error())) + return + } +} diff --git a/pkg/mcclient/options/compute/servers.go b/pkg/mcclient/options/compute/servers.go index d73e56ff43..2d571efc28 100644 --- a/pkg/mcclient/options/compute/servers.go +++ b/pkg/mcclient/options/compute/servers.go @@ -1311,6 +1311,16 @@ func (o *ServerChangeDiskStorageOptions) Params() (jsonutils.JSONObject, error) return jsonutils.Marshal(o), nil } +type ServerChangeStorageOptions struct { + options.BaseIdOptions + TARGETSTORAGE string `json:"target_storage_id" help:"Target storage id or name"` + KeepOriginDisk bool `json:"keep_origin_disk" help:"Keep origin disk when changed"` +} + +func (o *ServerChangeStorageOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(o), nil +} + type ServerCPUSetOptions struct { options.BaseIdOptions SETS string `help:"Cgroup cpusets CPUs spec string, e.g. '0-2,16'"`