diff --git a/cmd/climc/shell/compute/servers.go b/cmd/climc/shell/compute/servers.go index 4a022370be..0da12cf221 100644 --- a/cmd/climc/shell/compute/servers.go +++ b/cmd/climc/shell/compute/servers.go @@ -91,6 +91,7 @@ func init() { cmd.Perform("migrate-network", &options.ServerMigrateNetworkOptions{}) cmd.Perform("set-sshport", &options.ServerSetSshportOptions{}) cmd.Perform("have-agent", &options.ServerHaveAgentOptions{}) + cmd.Perform("change-disk-storage", &options.ServerChangeDiskStorageOptions{}) cmd.Get("vnc", new(options.ServerIdOptions)) cmd.Get("desc", new(options.ServerIdOptions)) diff --git a/pkg/apis/compute/disk_const.go b/pkg/apis/compute/disk_const.go index 12b5d5ee27..b4efbc63fe 100644 --- a/pkg/apis/compute/disk_const.go +++ b/pkg/apis/compute/disk_const.go @@ -43,6 +43,9 @@ const ( DISK_POST_MIGRATE = "post_migrate" DISK_MIGRATING = "migrating" + DISK_CLONE = "clone" + DISK_CLONE_FAIL = "clone_failed" + DISK_START_SNAPSHOT = "start_snapshot" DISK_SNAPSHOTING = "snapshoting" DISK_APPLY_SNAPSHOT_FAIL = "apply_snapshot_failed" diff --git a/pkg/apis/compute/guest_const.go b/pkg/apis/compute/guest_const.go index 840197ce74..ef93983433 100644 --- a/pkg/apis/compute/guest_const.go +++ b/pkg/apis/compute/guest_const.go @@ -77,15 +77,17 @@ const ( VM_REBUILD_ROOT = "rebuild_root" VM_REBUILD_ROOT_FAIL = "rebuild_root_fail" - VM_START_SNAPSHOT = "snapshot_start" - VM_SNAPSHOT = "snapshot" - VM_SNAPSHOT_DELETE = "snapshot_delete" - VM_BLOCK_STREAM = "block_stream" - VM_BLOCK_STREAM_FAIL = "block_stream_fail" - VM_SNAPSHOT_SUCC = "snapshot_succ" - VM_SNAPSHOT_FAILED = "snapshot_failed" - VM_DISK_RESET = "disk_reset" - VM_DISK_RESET_FAIL = "disk_reset_failed" + VM_START_SNAPSHOT = "snapshot_start" + VM_SNAPSHOT = "snapshot" + VM_SNAPSHOT_DELETE = "snapshot_delete" + VM_BLOCK_STREAM = "block_stream" + VM_BLOCK_STREAM_FAIL = "block_stream_fail" + VM_SNAPSHOT_SUCC = "snapshot_succ" + VM_SNAPSHOT_FAILED = "snapshot_failed" + VM_DISK_RESET = "disk_reset" + VM_DISK_RESET_FAIL = "disk_reset_failed" + VM_DISK_CHANGE_STORAGE = "disk_change_storage" + VM_DISK_CHANGE_STORAGE_FAIL = "disk_change_storage_fail" VM_START_INSTANCE_SNAPSHOT = "start_instance_snapshot" VM_INSTANCE_SNAPSHOT_FAILED = "instance_snapshot_failed" diff --git a/pkg/apis/compute/guests.go b/pkg/apis/compute/guests.go index 6252576043..0fc9db8227 100644 --- a/pkg/apis/compute/guests.go +++ b/pkg/apis/compute/guests.go @@ -688,3 +688,15 @@ type GuestJsonDesc struct { InstanceId string `json:"instance_id"` } `json:"instance_snapshot_info"` } + +type ServerChangeDiskStorageInput struct { + DiskId string `json:"disk_id"` + TargetStorageId string `json:"target_storage_id"` + KeepOriginDisk bool `json:"keep_origin_disk"` +} + +type ServerChangeDiskStorageInternalInput struct { + ServerChangeDiskStorageInput + StorageId string `json:"storage_id"` + TargetDiskId string `json:"target_disk_id"` +} diff --git a/pkg/apis/host/types.go b/pkg/apis/host/types.go new file mode 100644 index 0000000000..5b0d581ef9 --- /dev/null +++ b/pkg/apis/host/types.go @@ -0,0 +1,20 @@ +// 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 host + +type ServerCloneDiskFromStorageResponse struct { + TargetAccessPath string `json:"target_access_path"` + TargetFormat string `json:"target_format"` +} diff --git a/pkg/compute/guestdrivers/base.go b/pkg/compute/guestdrivers/base.go index 40f3a8be00..095a60a329 100644 --- a/pkg/compute/guestdrivers/base.go +++ b/pkg/compute/guestdrivers/base.go @@ -423,3 +423,20 @@ func (self *SBaseGuestDriver) ValidateRebuildRoot(ctx context.Context, userCred func (self *SBaseGuestDriver) ValidateDetachNetwork(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest) error { return nil } + +func (self *SBaseGuestDriver) ValidateChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, input *api.ServerChangeDiskStorageInput) error { + return cloudprovider.ErrNotImplemented +} + +func (self *SBaseGuestDriver) StartChangeDiskStorageTask(guest *models.SGuest, ctx context.Context, userCred mcclient.TokenCredential, params *api.ServerChangeDiskStorageInternalInput, parentTaskId string) error { + task, err := taskman.TaskManager.NewTask(ctx, "GuestChangeDiskStorageTask", guest, userCred, jsonutils.Marshal(params).(*jsonutils.JSONDict), parentTaskId, "", nil) + if err != nil { + return err + } + task.ScheduleRun(nil) + return nil +} + +func (self *SBaseGuestDriver) RequestChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, input *api.ServerChangeDiskStorageInternalInput, task taskman.ITask) error { + return cloudprovider.ErrNotImplemented +} diff --git a/pkg/compute/guestdrivers/kvm.go b/pkg/compute/guestdrivers/kvm.go index 95edf76e96..15525d523b 100644 --- a/pkg/compute/guestdrivers/kvm.go +++ b/pkg/compute/guestdrivers/kvm.go @@ -775,3 +775,44 @@ 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 { + // kvm guest must in ready status + if !utils.IsInStringArray(guest.Status, []string{api.VM_READY}) { + return httperrors.NewBadRequestError("Cannot change disk storage in status %s", guest.Status) + } + + // backup guest not supported + if guest.BackupHostId != "" { + return httperrors.NewBadRequestError("Cannot change disk storage in backup guest %s", guest.GetName()) + } + + // storage must attached on guest's host + host, err := guest.GetHost() + if err != nil { + return errors.Wrapf(err, "Get guest %s host", guest.GetName()) + } + attachedStorages := host.GetAttachedEnabledHostStorages(nil) + foundStorage := false + for _, storage := range attachedStorages { + if storage.GetId() == input.TargetStorageId { + foundStorage = true + } + } + if !foundStorage { + return httperrors.NewBadRequestError("Storage %s not attached or enabled on host %s", input.TargetStorageId, host.GetName()) + } + return nil +} + +func (self *SKVMGuestDriver) RequestChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, input *api.ServerChangeDiskStorageInternalInput, task taskman.ITask) error { + host, err := guest.GetHost() + if err != nil { + return err + } + body := jsonutils.Marshal(input) + header := self.getTaskRequestHeader(task) + url := fmt.Sprintf("%s/servers/%s/storage-clone-disk", host.ManagerUri, guest.GetId()) + _, _, err = httputils.JSONRequest(httputils.GetDefaultClient(), ctx, "POST", url, header, body, false) + return err +} diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index 49bf14d57c..56a5ecd238 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -5321,3 +5321,79 @@ func (self *SGuest) PerformListForward(ctx context.Context, userCred mcclient.To } return resp.JSON(), nil } + +func (self *SGuest) AllowPerformChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + + return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "change-disk-storage") +} + +func (self *SGuest) PerformChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input *api.ServerChangeDiskStorageInput) (*api.ServerChangeDiskStorageInput, error) { + // validate input + if input.DiskId == "" { + return nil, httperrors.NewNotEmptyError("Disk id is empty") + } + if input.TargetStorageId == "" { + return nil, httperrors.NewNotEmptyError("Storage id is empty") + } + + // validate disk + disks, err := self.GetDisks() + if err != nil { + return nil, errors.Wrapf(err, "Get server %s disks", self.GetName()) + } + var srcDisk *SDisk + for _, disk := range disks { + if input.DiskId == disk.GetId() || input.DiskId == disk.GetName() { + srcDisk = &disk + input.DiskId = disk.GetId() + break + } + } + if srcDisk == nil { + return nil, httperrors.NewNotFoundError("Disk %s not found on server %s", input.DiskId, self.GetName()) + } + + // 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() + + // driver validate + drv := self.GetDriver() + if err := drv.ValidateChangeDiskStorage(ctx, userCred, self, input); err != nil { + return nil, err + } + + // create a disk on target storage from source disk + diskConf := &api.DiskConfig{ + Index: -1, + ImageId: srcDisk.TemplateId, + Format: srcDisk.DiskFormat, + SizeMb: srcDisk.DiskSize, + Fs: srcDisk.FsFormat, + DiskType: srcDisk.DiskType, + } + + 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()) + } + + internalInput := &api.ServerChangeDiskStorageInternalInput{ + ServerChangeDiskStorageInput: *input, + StorageId: srcDisk.StorageId, + TargetDiskId: targetDisk.GetId(), + } + + return nil, self.StartChangeDiskStorageTask(ctx, userCred, internalInput, "") +} + +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, "") +} diff --git a/pkg/compute/models/guestdrivers.go b/pkg/compute/models/guestdrivers.go index 302910a305..fa9c687f03 100644 --- a/pkg/compute/models/guestdrivers.go +++ b/pkg/compute/models/guestdrivers.go @@ -210,6 +210,10 @@ type IGuestDriver interface { RequestOpenForward(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, req *guestdriver_types.OpenForwardRequest) (*guestdriver_types.OpenForwardResponse, error) 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 + 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 } var guestDrivers map[string]IGuestDriver diff --git a/pkg/compute/tasks/guest_change_disk_storage_task.go b/pkg/compute/tasks/guest_change_disk_storage_task.go new file mode 100644 index 0000000000..dd8822dbdf --- /dev/null +++ b/pkg/compute/tasks/guest_change_disk_storage_task.go @@ -0,0 +1,204 @@ +// 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/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/compute" + hostapi "yunion.io/x/onecloud/pkg/apis/host" + "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" +) + +func init() { + taskman.RegisterTask(GuestChangeDiskStorageTask{}) +} + +type GuestChangeDiskStorageTask struct { + SGuestBaseTask +} + +func (t *GuestChangeDiskStorageTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + guest := obj.(*models.SGuest) + t.ChangeDiskStorage(ctx, guest) +} + +func (t *GuestChangeDiskStorageTask) GetInputParams() (*api.ServerChangeDiskStorageInternalInput, error) { + input := new(api.ServerChangeDiskStorageInternalInput) + err := t.GetParams().Unmarshal(input) + return input, err +} + +func (t *GuestChangeDiskStorageTask) getDiskById(id string) (*models.SDisk, error) { + obj, err := models.DiskManager.FetchById(id) + if err != nil { + return nil, err + } + return obj.(*models.SDisk), nil +} + +func (t *GuestChangeDiskStorageTask) GetSourceDisk() (*models.SDisk, error) { + input, err := t.GetInputParams() + if err != nil { + return nil, errors.Wrap(err, "GetInputParams") + } + return t.getDiskById(input.DiskId) +} + +func (t *GuestChangeDiskStorageTask) GetTargetDisk() (*models.SDisk, error) { + input, err := t.GetInputParams() + if err != nil { + return nil, errors.Wrap(err, "GetInputParams") + } + return t.getDiskById(input.TargetDiskId) +} + +func (t *GuestChangeDiskStorageTask) ChangeDiskStorage(ctx context.Context, guest *models.SGuest) { + input, err := t.GetInputParams() + if err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("GetInputParams error: %v", err))) + return + } + + targetDisk, err := t.GetTargetDisk() + if err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("GetTargetDisk error: %v", err))) + return + } + // set target disk's status to clone + targetDisk.SetStatus(t.GetUserCred(), api.DISK_CLONE, "") + + t.SetStage("OnDiskChangeStorageComplete", nil) + if err := guest.GetDriver().RequestChangeDiskStorage(ctx, t.GetUserCred(), guest, input, t); err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("RequestChangeDiskStorage: %s", err))) + return + } +} + +func (t *GuestChangeDiskStorageTask) OnDiskChangeStorageComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + srcDisk, err := t.GetSourceDisk() + if err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("GetSourceDisk: %v", err))) + return + } + + // update target disk attributes by response + resp := new(hostapi.ServerCloneDiskFromStorageResponse) + if err := data.Unmarshal(resp); err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("Unmarshal response: %v", err))) + return + } + targetDisk, err := t.GetTargetDisk() + if err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("GetTargetDisk error: %v", err))) + return + } + if _, err := db.UpdateWithLock(ctx, targetDisk, func() error { + targetDisk.AccessPath = resp.TargetAccessPath + targetDisk.DiskFormat = resp.TargetFormat + return nil + }); err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("Update target disk attributes error: %v", err))) + return + } + + guestSrcDisk := guest.GetGuestDisk(srcDisk.GetId()) + if guestSrcDisk == nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("Source disk %s not attached", srcDisk.GetId()))) + return + } + + conf := guestSrcDisk.ToDiskConfig() + t.SetStage("OnSourceDiskDetachComplete", jsonutils.Marshal(conf).(*jsonutils.JSONDict)) + if err := t.detachSourceDisk(ctx, guest, srcDisk); err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("detachSourceDisk: %s", err))) + return + } +} + +func (t *GuestChangeDiskStorageTask) OnDiskChangeStorageCompleteFailed(ctx context.Context, guest *models.SGuest, err jsonutils.JSONObject) { + // set target disk's status to clone + targetDisk, _ := t.GetTargetDisk() + targetDisk.SetStatus(t.GetUserCred(), api.DISK_CLONE_FAIL, err.String()) + t.TaskFailed(ctx, guest, err) +} + +func (t *GuestChangeDiskStorageTask) detachSourceDisk(ctx context.Context, guest *models.SGuest, srcDisk *models.SDisk) error { + input, err := t.GetInputParams() + if err != nil { + return errors.Wrap(err, "GetInputParams") + } + return guest.StartGuestDetachdiskTask(ctx, t.GetUserCred(), srcDisk, input.KeepOriginDisk, t.GetTaskId(), false) +} + +func (t *GuestChangeDiskStorageTask) OnSourceDiskDetachComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + t.SetStage("OnTargetDiskAttachComplete", data.(*jsonutils.JSONDict)) + conf := new(api.DiskConfig) + if err := data.Unmarshal(conf); err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("unmarshal %s to api.DiskConfig: %s", data, err))) + return + } + if err := t.attachTargetDisk(ctx, guest, conf); err != nil { + t.TaskFailed(ctx, guest, jsonutils.NewString(fmt.Sprintf("attachTargetDisk: %s", err))) + return + } +} + +func (t *GuestChangeDiskStorageTask) OnSourceDiskDetachCompleteFailed(ctx context.Context, guest *models.SGuest, err jsonutils.JSONObject) { + t.TaskFailed(ctx, guest, err) +} + +func (t *GuestChangeDiskStorageTask) attachTargetDisk(ctx context.Context, guest *models.SGuest, conf *api.DiskConfig) error { + targetDisk, err := t.GetTargetDisk() + if err != nil { + return errors.Wrap(err, "GetTargetDisk") + } + confData := map[string]interface{}{ + "index": conf.Index, + "mountpoint": conf.Mountpoint, + "driver": conf.Driver, + "cache": conf.Cache, + } + attachData := jsonutils.Marshal(confData).(*jsonutils.JSONDict) + attachData.Add(jsonutils.NewString(targetDisk.GetId()), "disk_id") + + return guest.GetDriver().StartGuestAttachDiskTask(ctx, t.GetUserCred(), guest, attachData, t.GetTaskId()) +} + +func (t *GuestChangeDiskStorageTask) OnTargetDiskAttachComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + t.TaskComplete(ctx, guest, nil) +} + +func (t *GuestChangeDiskStorageTask) OnTargetDiskAttachCompleteFailed(ctx context.Context, guest *models.SGuest, err jsonutils.JSONObject) { + t.TaskFailed(ctx, guest, err) +} + +func (t *GuestChangeDiskStorageTask) 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 *GuestChangeDiskStorageTask) 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) +} diff --git a/pkg/hostman/guestman/guesthandlers/guesthandler.go b/pkg/hostman/guestman/guesthandlers/guesthandler.go index 4e08ec3199..d3b867f00a 100644 --- a/pkg/hostman/guestman/guesthandlers/guesthandler.go +++ b/pkg/hostman/guestman/guesthandlers/guesthandler.go @@ -23,6 +23,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" + computeapi "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/hostman/guestman" "yunion.io/x/onecloud/pkg/hostman/hostutils" @@ -81,6 +82,7 @@ func AddGuestTaskHandler(prefix string, app *appsrv.Application) { "open-forward": guestOpenForward, "list-forward": guestListForward, "close-forward": guestCloseForward, + "storage-clone-disk": guestStorageCloneDisk, } { app.AddHandler("POST", fmt.Sprintf("%s/%s//%s", prefix, keyWord, action), @@ -567,3 +569,35 @@ func guestDeleteSnapshot(ctx context.Context, sid string, body jsonutils.JSONObj hostutils.DelayTask(ctx, guestman.GetGuestManager().DeleteSnapshot, params) return nil, nil } + +func guestStorageCloneDisk(ctx context.Context, sid string, body jsonutils.JSONObject) (interface{}, error) { + input := new(computeapi.ServerChangeDiskStorageInternalInput) + if err := body.Unmarshal(input); err != nil { + return nil, err + } + srcStorage := storageman.GetManager().GetStorage(input.StorageId) + if srcStorage == nil { + return nil, httperrors.NewNotFoundError("Source storage %q not found", input.StorageId) + } + srcDisk, err := srcStorage.GetDiskById(input.DiskId) + if err != nil { + return nil, errors.Wrapf(err, "Get source disk %q on storage %q", input.DiskId, srcStorage.GetId()) + } + targetStorage := storageman.GetManager().GetStorage(input.TargetStorageId) + if targetStorage == nil { + return nil, httperrors.NewNotFoundError("Target storage %s not found", input.TargetStorageId) + } + if input.TargetDiskId == "" { + return nil, httperrors.NewMissingParameterError("Target disk id is empty") + } + + params := &guestman.SStorageCloneDisk{ + ServerId: sid, + SourceStorage: srcStorage, + SourceDisk: srcDisk, + TargetStorage: targetStorage, + TargetDiskId: input.TargetDiskId, + } + hostutils.DelayTaskWithoutReqctx(ctx, guestman.GetGuestManager().StorageCloneDisk, params) + return nil, nil +} diff --git a/pkg/hostman/guestman/guestman.go b/pkg/hostman/guestman/guestman.go index 8d7dea5297..0844640ddb 100644 --- a/pkg/hostman/guestman/guestman.go +++ b/pkg/hostman/guestman/guestman.go @@ -1012,6 +1012,27 @@ func (m *SGuestManager) ExitGuestCleanup() { } } +type SStorageCloneDisk struct { + ServerId string + SourceStorage storageman.IStorage + SourceDisk storageman.IDisk + TargetStorage storageman.IStorage + TargetDiskId string +} + +func (m *SGuestManager) StorageCloneDisk(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { + input := params.(*SStorageCloneDisk) + guest, _ := m.GetServer(input.ServerId) + if guest == nil { + return nil, httperrors.NewNotFoundError("Not found guest by id %s", input.ServerId) + } + if guest.IsRunning() || guest.IsSuspend() { + return nil, httperrors.NewBadRequestError("Cannot change disk storage on running/suspend guest") + } + NewGuestStorageCloneDiskTask(guest, input).Start(ctx) + return nil, nil +} + func (m *SGuestManager) GetHost() hostutils.IHost { return m.host } diff --git a/pkg/hostman/guestman/guesttasks.go b/pkg/hostman/guestman/guesttasks.go index 50f96ac8c8..4fcdf0484d 100644 --- a/pkg/hostman/guestman/guesttasks.go +++ b/pkg/hostman/guestman/guesttasks.go @@ -25,6 +25,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" + "yunion.io/x/pkg/errors" "yunion.io/x/pkg/utils" "yunion.io/x/onecloud/pkg/appctx" @@ -1455,3 +1456,26 @@ func (task *SCancelBlockJobs) taskComplete() { hostutils.TaskComplete(task.ctx, nil) } } + +type SGuestStorageCloneDiskTask struct { + guest *SKVMGuestInstance + params *SStorageCloneDisk +} + +func NewGuestStorageCloneDiskTask(guest *SKVMGuestInstance, params *SStorageCloneDisk) *SGuestStorageCloneDiskTask { + return &SGuestStorageCloneDiskTask{ + guest: guest, + params: params, + } +} + +func (t *SGuestStorageCloneDiskTask) Start(ctx context.Context) { + resp, err := t.params.TargetStorage.CloneDiskFromStorage(ctx, t.params.SourceStorage, t.params.SourceDisk, t.params.TargetDiskId) + if err != nil { + hostutils.TaskFailed( + ctx, + errors.Wrapf(err, "Clone disk %s to storage %s", t.params.SourceDisk.GetPath(), t.params.TargetStorage.GetId()).Error()) + return + } + hostutils.TaskComplete(ctx, jsonutils.Marshal(resp)) +} diff --git a/pkg/hostman/storageman/disk_rbd.go b/pkg/hostman/storageman/disk_rbd.go index ab6e0d39fb..3bef06c100 100644 --- a/pkg/hostman/storageman/disk_rbd.go +++ b/pkg/hostman/storageman/disk_rbd.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build linux && cgo // +build linux,cgo package storageman @@ -58,9 +59,7 @@ func (d *SRBDDisk) Probe() error { } func (d *SRBDDisk) getPath() string { - storageConf := d.Storage.GetStorageConf() - pool, _ := storageConf.GetString("pool") - return fmt.Sprintf("rbd:%s/%s", pool, d.Id) + return d.Storage.(*SRbdStorage).getDiskPath(d.Id) } func (d *SRBDDisk) GetPath() string { diff --git a/pkg/hostman/storageman/storage_base.go b/pkg/hostman/storageman/storage_base.go index 8f2cf70cc2..5a74e4869e 100644 --- a/pkg/hostman/storageman/storage_base.go +++ b/pkg/hostman/storageman/storage_base.go @@ -29,6 +29,7 @@ import ( "yunion.io/x/log" "yunion.io/x/pkg/errors" + "yunion.io/x/onecloud/pkg/apis/host" "yunion.io/x/onecloud/pkg/cloudcommon/cronman" "yunion.io/x/onecloud/pkg/hostman/hostutils" "yunion.io/x/onecloud/pkg/hostman/options" @@ -116,6 +117,11 @@ type IStorage interface { SaveToGlance(context.Context, interface{}) (jsonutils.JSONObject, error) CreateDiskFromSnapshot(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 + CloneDiskFromStorage(ctx context.Context, srcStorage IStorage, srcDisk IDisk, targetDiskId string) (*host.ServerCloneDiskFromStorageResponse, error) + CreateSnapshotFormUrl(ctx context.Context, snapshotUrl, diskId, snapshotPath string) error DeleteDiskfile(diskPath string) error @@ -364,6 +370,14 @@ func (s *SBaseStorage) DestinationPrepareMigrate( return nil } +func (s *SBaseStorage) GetCloneTargetDiskPath(ctx context.Context, targetDiskId string) string { + return "" +} + +func (s *SBaseStorage) CloneDiskFromStorage(ctx context.Context, srcStorage IStorage, srcDisk IDisk, targetDiskId string) (*host.ServerCloneDiskFromStorageResponse, error) { + return nil, httperrors.ErrNotImplemented +} + /*************************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 98b8f9ca33..c5b58f602f 100644 --- a/pkg/hostman/storageman/storage_local.go +++ b/pkg/hostman/storageman/storage_local.go @@ -29,6 +29,7 @@ import ( "yunion.io/x/pkg/util/timeutils" api "yunion.io/x/onecloud/pkg/apis/compute" + hostapi "yunion.io/x/onecloud/pkg/apis/host" "yunion.io/x/onecloud/pkg/cloudprovider" deployapi "yunion.io/x/onecloud/pkg/hostman/hostdeployer/apis" "yunion.io/x/onecloud/pkg/hostman/hostdeployer/deployclient" @@ -502,3 +503,24 @@ func (s *SLocalStorage) CreateDiskFromSnapshot( } return httperrors.NewUnsupportOperationError("Unsupport protocol %s for Local storage", info.Protocol) } + +func (s *SLocalStorage) GetCloneTargetDiskPath(ctx context.Context, targetDiskId string) string { + return path.Join(s.GetPath(), targetDiskId) +} + +func (s *SLocalStorage) CloneDiskFromStorage(ctx context.Context, srcStorage IStorage, srcDisk IDisk, targetDiskId string) (*hostapi.ServerCloneDiskFromStorageResponse, error) { + srcDiskPath := srcDisk.GetPath() + srcImg, err := qemuimg.NewQemuImage(srcDiskPath) + if err != nil { + return nil, errors.Wrapf(err, "Get source image %q info", srcDiskPath) + } + accessPath := s.GetCloneTargetDiskPath(ctx, targetDiskId) + _, err = srcImg.Clone(s.GetCloneTargetDiskPath(ctx, targetDiskId), qemuimg.QCOW2, false) + if err != nil { + return nil, errors.Wrap(err, "Clone source disk to target local storage") + } + return &hostapi.ServerCloneDiskFromStorageResponse{ + TargetAccessPath: accessPath, + TargetFormat: qemuimg.QCOW2.String(), + }, nil +} diff --git a/pkg/hostman/storageman/storage_nas.go b/pkg/hostman/storageman/storage_nas.go index fa850dd170..d75e66aa53 100644 --- a/pkg/hostman/storageman/storage_nas.go +++ b/pkg/hostman/storageman/storage_nas.go @@ -22,8 +22,10 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" + hostapi "yunion.io/x/onecloud/pkg/apis/host" "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/hostman/hostutils" + "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient/modules" ) @@ -96,3 +98,7 @@ func (s *SNasStorage) SyncStorageInfo() (jsonutils.JSONObject, error) { func (s *SNasStorage) CreateDiskFromSnapshot(ctx context.Context, disk IDisk, input *SDiskCreateByDiskinfo) error { return disk.CreateFromSnapshotLocation(ctx, input.DiskInfo.SnapshotUrl, int64(input.DiskInfo.DiskSizeMb)) } + +func (s *SNasStorage) CloneDiskFromStorage(ctx context.Context, srcStorage IStorage, srcDisk IDisk, targetDiskId string) (*hostapi.ServerCloneDiskFromStorageResponse, error) { + return nil, httperrors.ErrNotImplemented +} diff --git a/pkg/hostman/storageman/storage_rbd.go b/pkg/hostman/storageman/storage_rbd.go index 9a31696161..f61c2af8bf 100644 --- a/pkg/hostman/storageman/storage_rbd.go +++ b/pkg/hostman/storageman/storage_rbd.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build linux && cgo // +build linux,cgo package storageman @@ -29,6 +30,7 @@ import ( "yunion.io/x/pkg/utils" api "yunion.io/x/onecloud/pkg/apis/compute" + hostapi "yunion.io/x/onecloud/pkg/apis/host" "yunion.io/x/onecloud/pkg/cloudprovider" deployapi "yunion.io/x/onecloud/pkg/hostman/hostdeployer/apis" "yunion.io/x/onecloud/pkg/hostman/hostdeployer/deployclient" @@ -37,6 +39,7 @@ import ( "yunion.io/x/onecloud/pkg/mcclient/modules" "yunion.io/x/onecloud/pkg/util/cephutils" "yunion.io/x/onecloud/pkg/util/procutils" + "yunion.io/x/onecloud/pkg/util/qemuimg" "yunion.io/x/onecloud/pkg/util/qemutils" ) @@ -521,6 +524,37 @@ func (s *SRbdStorage) CreateDiskFromSnapshot(ctx context.Context, disk IDisk, in return disk.CreateFromRbdSnapshot(ctx, info.SnapshotUrl, info.SrcDiskId, info.SrcPool) } +func (s *SRbdStorage) getDiskPath(diskId string) string { + storageConf := s.GetStorageConf() + pool, _ := storageConf.GetString("pool") + return fmt.Sprintf("rbd:%s/%s", pool, diskId) +} + +func (s *SRbdStorage) GetDiskPath(diskId string) string { + return fmt.Sprintf("%s%s", s.getDiskPath(diskId), s.getStorageConfString()) +} + +func (s *SRbdStorage) GetCloneTargetDiskPath(ctx context.Context, targetDiskId string) string { + return s.GetDiskPath(targetDiskId) +} + +func (s *SRbdStorage) CloneDiskFromStorage(ctx context.Context, srcStorage IStorage, srcDisk IDisk, targetDiskId string) (*hostapi.ServerCloneDiskFromStorageResponse, error) { + srcDiskPath := srcDisk.GetPath() + srcImg, err := qemuimg.NewQemuImage(srcDiskPath) + if err != nil { + return nil, errors.Wrapf(err, "Get source image %q info", srcDiskPath) + } + accessPath := s.GetCloneTargetDiskPath(ctx, targetDiskId) + _, err = srcImg.Clone(accessPath, qemuimg.RAW, false) + if err != nil { + return nil, errors.Wrap(err, "Clone source disk to target rbd storage") + } + return &hostapi.ServerCloneDiskFromStorageResponse{ + TargetAccessPath: accessPath, + TargetFormat: qemuimg.RAW.String(), + }, nil +} + func (s *SRbdStorage) SetStorageInfo(storageId, storageName string, conf jsonutils.JSONObject) error { s.StorageId = storageId s.StorageName = storageName diff --git a/pkg/mcclient/options/servers.go b/pkg/mcclient/options/servers.go index 9aa74459b2..aacc7a2aac 100644 --- a/pkg/mcclient/options/servers.go +++ b/pkg/mcclient/options/servers.go @@ -1162,3 +1162,14 @@ type ServerDomainStatisticsOptions struct { ServerListOptions DomainStatisticsOptions } + +type ServerChangeDiskStorageOptions struct { + BaseIdOptions + DISKID string `json:"disk_id" help:"Disk id or name"` + 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 *ServerChangeDiskStorageOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(o), nil +} diff --git a/pkg/util/cephutils/ceph.go b/pkg/util/cephutils/ceph.go index f486a778d7..ff3afde8c9 100644 --- a/pkg/util/cephutils/ceph.go +++ b/pkg/util/cephutils/ceph.go @@ -91,11 +91,36 @@ type SCapacity struct { func (self *CephClient) output(name string, opts []string) (jsonutils.JSONObject, error) { opts = append([]string{"--format", "json"}, opts...) - resp, err := procutils.NewRemoteCommandAsFarAsPossible(name, opts...).Output() + proc := procutils.NewRemoteCommandAsFarAsPossible(name, opts...) + outb, err := proc.StdoutPipe() if err != nil { - return nil, errors.Wrapf(err, "%s %s", name, string(resp)) + return nil, errors.Wrap(err, "stdout pipe") } - return jsonutils.Parse(resp) + defer outb.Close() + + errb, err := proc.StderrPipe() + if err != nil { + return nil, errors.Wrap(err, "stderr pipe") + } + defer errb.Close() + + if err := proc.Start(); err != nil { + return nil, errors.Wrap(err, "start ceph process") + } + + stdoutPut, err := ioutil.ReadAll(outb) + if err != nil { + return nil, err + } + stderrPut, err := ioutil.ReadAll(errb) + if err != nil { + return nil, err + } + + if err := proc.Wait(); err != nil { + return nil, errors.Wrapf(err, "stderr %q", stderrPut) + } + return jsonutils.Parse(stdoutPut) } func (self *CephClient) run(name string, opts []string) error { diff --git a/pkg/util/logclient/consts.go b/pkg/util/logclient/consts.go index 50895d9b04..65cc833d5a 100644 --- a/pkg/util/logclient/consts.go +++ b/pkg/util/logclient/consts.go @@ -79,6 +79,7 @@ const ( ACT_EIP_CONVERT = "eip_convert" ACT_CHANGE_BANDWIDTH = "change_bandwidth" ACT_DISK_CREATE_SNAPSHOT = "disk_create_snapshot" + ACT_DISK_CHANGE_STORAGE = "disk_change_storage" ACT_LB_ADD_BACKEND = "lb_add_backend" ACT_LB_REMOVE_BACKEND = "lb_remove_backend" ACL_LB_SYNC_BACKEND_CONF = "lb_sync_backend_conf"