diff --git a/pkg/apis/compute/disk.go b/pkg/apis/compute/disk.go index 7302b6d747..14f917989f 100644 --- a/pkg/apis/compute/disk.go +++ b/pkg/apis/compute/disk.go @@ -242,3 +242,11 @@ type DiskUpdateInput struct { // 磁盘类型 DiskType string `json:"disk_type"` } + +type DiskSaveInput struct { + Name string + Format string + + // swagger: ignore + ImageId string +} diff --git a/pkg/apis/compute/guests.go b/pkg/apis/compute/guests.go index f878585374..2a92c2c24d 100644 --- a/pkg/apis/compute/guests.go +++ b/pkg/apis/compute/guests.go @@ -453,3 +453,28 @@ type ServerStopInput struct { // 目前仅阿里云,腾讯云此参数生效 StopCharging bool `json:"stop_charging"` } + +type ServerSaveImageInput struct { + // 镜像名称 + Name string + GenerateName string + Notes string + IsPublic bool + // 镜像格式 + Format string + + // 保存镜像后是否自动启动,若实例状态为运行中,则会先关闭实例 + // default: false + AutoStart bool + // swagger: ignore + Restart bool + + // swagger: ignore + OsType string + + // swagger: ignore + OsArch string + + // swagger: ignore + ImageId string +} diff --git a/pkg/cloudprovider/images.go b/pkg/cloudprovider/images.go index cc70899b5c..c26e6d2ea4 100644 --- a/pkg/cloudprovider/images.go +++ b/pkg/cloudprovider/images.go @@ -65,6 +65,11 @@ type SSubImage struct { SizeBytes int64 } +type SaveImageOptions struct { + Name string + Notes string +} + func CloudImage2Image(image ICloudImage) SImage { return SImage{ CreatedAt: image.GetCreatedAt(), diff --git a/pkg/cloudprovider/resources.go b/pkg/cloudprovider/resources.go index 7940fff77e..650f482abd 100644 --- a/pkg/cloudprovider/resources.go +++ b/pkg/cloudprovider/resources.go @@ -330,6 +330,8 @@ type ICloudVM interface { GetInstanceSnapshot(idStr string) (ICloudInstanceSnapshot, error) GetInstanceSnapshots() ([]ICloudInstanceSnapshot, error) ResetToInstanceSnapshot(ctx context.Context, idStr string) error + + SaveImage(opts *SaveImageOptions) (ICloudImage, error) } type ICloudNic interface { diff --git a/pkg/compute/guestdrivers/base.go b/pkg/compute/guestdrivers/base.go index a99ad0b3fb..a5c8327410 100644 --- a/pkg/compute/guestdrivers/base.go +++ b/pkg/compute/guestdrivers/base.go @@ -21,6 +21,7 @@ import ( "time" "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudcommon/db/quotas" @@ -114,6 +115,10 @@ func (self *SBaseGuestDriver) RequestAttachDisk(ctx context.Context, guest *mode return nil } +func (self *SBaseGuestDriver) RequestSaveImage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, task taskman.ITask) error { + return errors.Wrapf(cloudprovider.ErrNotImplemented, "RequestSaveImage") +} + func (self *SBaseGuestDriver) RequestGuestCreateAllDisks(ctx context.Context, guest *models.SGuest, task taskman.ITask) error { return fmt.Errorf("Not Implement") } diff --git a/pkg/compute/guestdrivers/esxi.go b/pkg/compute/guestdrivers/esxi.go index a043bc18d0..2483a95110 100644 --- a/pkg/compute/guestdrivers/esxi.go +++ b/pkg/compute/guestdrivers/esxi.go @@ -507,6 +507,13 @@ func (self *SESXiGuestDriver) AllowReconfigGuest() bool { return true } +func (self *SESXiGuestDriver) RequestSaveImage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, task taskman.ITask) error { + disks := guest.CategorizeDisks() + opts := api.DiskSaveInput{} + task.GetParams().Unmarshal(&opts) + return disks.Root.StartDiskSaveTask(ctx, userCred, opts, task.GetTaskId()) +} + func (self *SESXiGuestDriver) DoGuestCreateDisksTask(ctx context.Context, guest *models.SGuest, task taskman.ITask) error { subtask, err := taskman.TaskManager.NewTask(ctx, "ESXiGuestCreateDiskTask", guest, task.GetUserCred(), task.GetParams(), task.GetTaskId(), "", nil) if err != nil { diff --git a/pkg/compute/guestdrivers/kvm.go b/pkg/compute/guestdrivers/kvm.go index 1d7349d372..54c3617741 100644 --- a/pkg/compute/guestdrivers/kvm.go +++ b/pkg/compute/guestdrivers/kvm.go @@ -437,6 +437,13 @@ func (self *SKVMGuestDriver) RequestAttachDisk(ctx context.Context, guest *model return guest.StartSyncTask(ctx, task.GetUserCred(), false, task.GetTaskId()) } +func (self *SKVMGuestDriver) RequestSaveImage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, task taskman.ITask) error { + disks := guest.CategorizeDisks() + opts := api.DiskSaveInput{} + task.GetParams().Unmarshal(&opts) + return disks.Root.StartDiskSaveTask(ctx, userCred, opts, task.GetTaskId()) +} + func (self *SKVMGuestDriver) GetDetachDiskStatus() ([]string, error) { return []string{api.VM_READY, api.VM_RUNNING}, nil } diff --git a/pkg/compute/guestdrivers/managedvirtual.go b/pkg/compute/guestdrivers/managedvirtual.go index 3bf9f81abd..26cc55825e 100644 --- a/pkg/compute/guestdrivers/managedvirtual.go +++ b/pkg/compute/guestdrivers/managedvirtual.go @@ -133,6 +133,50 @@ func (self *SManagedVirtualizedGuestDriver) GetJsonDescAtHost(ctx context.Contex return jsonutils.Marshal(&config), nil } +func (self *SManagedVirtualizedGuestDriver) RequestSaveImage(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, task taskman.ITask) error { + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + iVm, err := guest.GetIVM() + if err != nil { + return nil, errors.Wrapf(err, "guest.GetIVM") + } + opts := &cloudprovider.SaveImageOptions{} + err = task.GetParams().Unmarshal(opts) + image, err := iVm.SaveImage(opts) + if err != nil { + return nil, errors.Wrapf(err, "iVm.SaveImage") + } + err = cloudprovider.WaitStatus(image, cloudprovider.IMAGE_STATUS_ACTIVE, time.Second*10, time.Minute*10) + if err != nil { + return nil, errors.Wrapf(err, "wait image %s(%s) active current is: %s", image.GetName(), image.GetGlobalId(), image.GetStatus()) + } + host := guest.GetHost() + if host == nil { + return nil, errors.Wrapf(cloudprovider.ErrNotFound, "find guest %s host", guest.Name) + } + region := host.GetRegion() + iRegion, err := host.GetIRegion() + if err != nil { + return nil, errors.Wrapf(err, "host.GetIRegion") + } + caches, err := region.GetStoragecaches() + if err != nil { + return nil, errors.Wrapf(err, "region.GetStoragecaches") + } + for i := range caches { + if caches[i].ManagerId == host.ManagerId { + iStoragecache, err := iRegion.GetIStoragecacheById(caches[i].ExternalId) + if err != nil { + return nil, errors.Wrapf(err, "iRegion.GetIStoragecacheById(%s)", caches[i].ExternalId) + } + result := caches[i].SyncCloudImages(ctx, userCred, iStoragecache, region) + log.Infof("sync cloud image for storagecache %s result: %s", caches[i].Name, result.Result()) + } + } + return nil, nil + }) + return nil +} + func (self *SManagedVirtualizedGuestDriver) RequestGuestCreateAllDisks(ctx context.Context, guest *models.SGuest, task taskman.ITask) error { diskCat := guest.CategorizeDisks() var imageId string diff --git a/pkg/compute/models/disks.go b/pkg/compute/models/disks.go index d274cbb288..5014fc94a6 100644 --- a/pkg/compute/models/disks.go +++ b/pkg/compute/models/disks.go @@ -989,23 +989,40 @@ func (self *SDisk) GetZone() *SZone { return nil } -func (self *SDisk) PrepareSaveImage(ctx context.Context, userCred mcclient.TokenCredential, data *jsonutils.JSONDict) (string, error) { - if zone := self.GetZone(); zone == nil { +func (self *SDisk) PrepareSaveImage(ctx context.Context, userCred mcclient.TokenCredential, input api.ServerSaveImageInput) (string, error) { + zone := self.GetZone() + if zone == nil { return "", httperrors.NewResourceNotFoundError("No zone for this disk") } - data.Add(jsonutils.NewString(self.DiskFormat), "disk_format") - if !data.Contains("generate_name") { - name, _ := data.GetString("name") + if len(input.GenerateName) == 0 { s := auth.GetAdminSession(ctx, options.Options.Region, "") - imageList, err := modules.Images.List(s, jsonutils.Marshal(map[string]string{"name": name, "admin": "true"})) + imageList, err := modules.Images.List(s, jsonutils.Marshal(map[string]string{"name": input.Name, "admin": "true"})) if err != nil { return "", err } if imageList.Total > 0 { - return "", httperrors.NewConflictError("Duplicate image name %s", name) + return "", httperrors.NewConflictError("Duplicate image name %s", input.Name) } } + opts := struct { + Name string + GenerateName string + VirtualSize int + DiskFormat string + Properties map[string]string + }{ + Name: input.Name, + GenerateName: input.GenerateName, + VirtualSize: self.DiskSize, + DiskFormat: self.DiskFormat, + Properties: map[string]string{ + "notes": input.Notes, + "os_type": input.OsType, + "os_arch": input.OsArch, + }, + } + /* no need to check quota anymore session := auth.GetSession(userCred, options.Options.Region, "v2") @@ -1014,8 +1031,7 @@ func (self *SDisk) PrepareSaveImage(ctx context.Context, userCred mcclient.Token return "", err }*/ us := auth.GetSession(ctx, userCred, options.Options.Region, "") - data.Add(jsonutils.NewInt(int64(self.DiskSize)), "virtual_size") - result, err := modules.Images.Create(us, data) + result, err := modules.Images.Create(us, jsonutils.Marshal(opts)) if err != nil { return "", err } @@ -1030,7 +1046,7 @@ func (self *SDisk) AllowPerformSave(ctx context.Context, userCred mcclient.Token return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "save") } -func (self *SDisk) PerformSave(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { +func (self *SDisk) PerformSave(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.DiskSaveInput) (jsonutils.JSONObject, error) { if self.Status != api.DISK_READY { return nil, httperrors.NewResourceNotReadyError("Save disk when disk is READY") @@ -1043,26 +1059,27 @@ func (self *SDisk) PerformSave(ctx context.Context, userCred mcclient.TokenCrede return nil, httperrors.NewResourceNotReadyError("Save disk when not being USED") } - if name, err := data.GetString("name"); err != nil || len(name) == 0 { + if len(input.Name) == 0 { return nil, httperrors.NewInputParameterError("Image name is required") } - kwargs := data.(*jsonutils.JSONDict) - if imageId, err := self.PrepareSaveImage(ctx, userCred, kwargs); err != nil { - return nil, err - } else { - kwargs.Add(jsonutils.NewString(imageId), "image_id") - return nil, self.StartDiskSaveTask(ctx, userCred, kwargs, "") + opts := api.ServerSaveImageInput{ + Name: input.Name, } + input.ImageId, err = self.PrepareSaveImage(ctx, userCred, opts) + if err != nil { + return nil, errors.Wrapf(err, "PrepareSaveImage") + } + return nil, self.StartDiskSaveTask(ctx, userCred, input, "") } -func (self *SDisk) StartDiskSaveTask(ctx context.Context, userCred mcclient.TokenCredential, data *jsonutils.JSONDict, parentTaskId string) error { - self.SetStatus(userCred, api.DISK_START_SAVE, "") - if task, err := taskman.TaskManager.NewTask(ctx, "DiskSaveTask", self, userCred, data, parentTaskId, "", nil); err != nil { - log.Errorf("Start DiskSaveTask failed:%v", err) - return err - } else { - task.ScheduleRun(nil) +func (self *SDisk) StartDiskSaveTask(ctx context.Context, userCred mcclient.TokenCredential, input api.DiskSaveInput, parentTaskId string) error { + data := jsonutils.Marshal(input).(*jsonutils.JSONDict) + task, err := taskman.TaskManager.NewTask(ctx, "DiskSaveTask", self, userCred, data, parentTaskId, "", nil) + if err != nil { + return errors.Wrapf(err, "NewTask") } + self.SetStatus(userCred, api.DISK_START_SAVE, "") + task.ScheduleRun(nil) return nil } diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index 0c15a21ec0..085e7a7f61 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -50,6 +50,7 @@ import ( "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient" "yunion.io/x/onecloud/pkg/cloudcommon/policy" "yunion.io/x/onecloud/pkg/cloudcommon/userdata" + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/compute/options" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" @@ -178,51 +179,50 @@ func (self *SGuest) AllowPerformSaveImage(ctx context.Context, userCred mcclient return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "save-image") } -func (self *SGuest) PerformSaveImage(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { - if !utils.IsInStringArray(self.Status, []string{api.VM_READY}) { +func (self *SGuest) PerformSaveImage(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ServerSaveImageInput) (jsonutils.JSONObject, error) { + if !utils.IsInStringArray(self.Status, []string{api.VM_READY, api.VM_RUNNING}) { return nil, httperrors.NewInputParameterError("Cannot save image in status %s", self.Status) - } else if !data.Contains("name") && !data.Contains("generate_name") { + } + input.Restart = (self.Status == api.VM_RUNNING) || input.AutoStart + if len(input.Name) == 0 && len(input.GenerateName) == 0 { return nil, httperrors.NewInputParameterError("Image name is required") - } else if disks := self.CategorizeDisks(); disks.Root == nil { + } + disks := self.CategorizeDisks() + if disks.Root == nil { return nil, httperrors.NewInputParameterError("No root image") - } else { - kwargs := data.(*jsonutils.JSONDict) - restart := (self.Status == api.VM_RUNNING) || jsonutils.QueryBoolean(data, "auto_start", false) - properties := jsonutils.NewDict() - if notes, err := data.GetString("notes"); err != nil && len(notes) > 0 { - properties.Add(jsonutils.NewString(notes), "notes") + } + input.OsType = self.OsType + if len(input.OsType) == 0 { + input.OsType = "Linux" + } + input.OsArch = self.OsArch + if self.OsArch == api.OS_ARCH_ARM { + if osArch := self.GetMetadata("os_arch", nil); len(osArch) == 0 { + host := self.GetHost() + input.OsArch = host.CpuArchitecture } - osType := self.OsType - if len(osType) == 0 { - osType = "Linux" - } - properties.Add(jsonutils.NewString(osType), "os_type") - if self.OsArch == api.OS_ARCH_ARM { - var osArch string - if osArch = self.GetMetadata("os_arch", nil); len(osArch) == 0 { - host := self.GetHost() - osArch = host.CpuArchitecture - } - properties.Add(jsonutils.NewString(osArch), "os_arch") - kwargs.Set("os_arch", jsonutils.NewString(self.OsArch)) - } - kwargs.Add(properties, "properties") - kwargs.Add(jsonutils.NewBool(restart), "restart") + } + factory, _ := cloudprovider.GetProviderFactory(self.GetDriver().GetProvider()) + if factory == nil || factory.IsOnPremise() { // OneCloud or VMware lockman.LockObject(ctx, disks.Root) defer lockman.ReleaseObject(ctx, disks.Root) - if imageId, err := disks.Root.PrepareSaveImage(ctx, userCred, kwargs); err != nil { - return nil, err - } else { - kwargs.Add(jsonutils.NewString(imageId), "image_id") + var err error + input.ImageId, err = disks.Root.PrepareSaveImage(ctx, userCred, input) + if err != nil { + return nil, errors.Wrapf(err, "PrepareSaveImage") } - return nil, self.StartGuestSaveImage(ctx, userCred, kwargs, "") } + if len(input.Name) == 0 { + input.Name = input.GenerateName + } + + return nil, self.StartGuestSaveImage(ctx, userCred, input, "") } -func (self *SGuest) StartGuestSaveImage(ctx context.Context, userCred mcclient.TokenCredential, data *jsonutils.JSONDict, parentTaskId string) error { - return self.GetDriver().StartGuestSaveImage(ctx, userCred, self, data, parentTaskId) +func (self *SGuest) StartGuestSaveImage(ctx context.Context, userCred mcclient.TokenCredential, input api.ServerSaveImageInput, parentTaskId string) error { + return self.GetDriver().StartGuestSaveImage(ctx, userCred, self, jsonutils.Marshal(input).(*jsonutils.JSONDict), parentTaskId) } func (self *SGuest) AllowPerformSaveGuestImage(ctx context.Context, userCred mcclient.TokenCredential, diff --git a/pkg/compute/models/guestdrivers.go b/pkg/compute/models/guestdrivers.go index ac56872473..f772b5b200 100644 --- a/pkg/compute/models/guestdrivers.go +++ b/pkg/compute/models/guestdrivers.go @@ -116,6 +116,7 @@ type IGuestDriver interface { StartGuestSaveImage(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, params *jsonutils.JSONDict, parentTaskId string) error StartGuestSaveGuestImage(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, params *jsonutils.JSONDict, parentTaskId string) error + RequestSaveImage(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, task taskman.ITask) error RequestStopGuestForDelete(ctx context.Context, guest *SGuest, host *SHost, task taskman.ITask) error diff --git a/pkg/compute/tasks/guest_save_image_task.go b/pkg/compute/tasks/guest_save_image_task.go index df86d41fcd..f8529edee4 100644 --- a/pkg/compute/tasks/guest_save_image_task.go +++ b/pkg/compute/tasks/guest_save_image_task.go @@ -37,29 +37,33 @@ func init() { func (self *GuestSaveImageTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) { guest := obj.(*models.SGuest) log.Infof("Saving server image: %s", guest.Name) - if restart, _ := self.GetParams().Bool("restart"); restart { + restart := jsonutils.QueryBoolean(self.Params, "restart", false) + if restart { self.SetStage("OnStopServerComplete", nil) guest.StartGuestStopTask(ctx, self.GetUserCred(), false, false, self.GetTaskId()) - } else { - self.OnStopServerComplete(ctx, guest, nil) + return } + self.OnStopServerComplete(ctx, guest, nil) } func (self *GuestSaveImageTask) OnStopServerComplete(ctx context.Context, guest *models.SGuest, body jsonutils.JSONObject) { self.SetStage("OnSaveRootImageComplete", nil) - disks := guest.CategorizeDisks() - if err := disks.Root.StartDiskSaveTask(ctx, self.GetUserCred(), self.GetParams(), self.GetTaskId()); err != nil { - self.SetStageFailed(ctx, jsonutils.NewString(err.Error())) + err := guest.GetDriver().RequestSaveImage(ctx, self.GetUserCred(), guest, self) + if err != nil { + self.OnSaveRootImageCompleteFailed(ctx, guest, jsonutils.NewString(err.Error())) + return } } func (self *GuestSaveImageTask) OnSaveRootImageComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { - if restart, _ := self.GetParams().Bool("restart"); restart { + restart := jsonutils.QueryBoolean(self.Params, "restart", false) + if restart { self.SetStage("OnStartServerComplete", nil) guest.StartGueststartTask(ctx, self.GetUserCred(), nil, self.GetTaskId()) - } else { - self.SetStageComplete(ctx, nil) + return } + self.SetStage("OnSyncstatusComplete", nil) + guest.StartSyncstatus(ctx, self.GetUserCred(), self.GetTaskId()) } func (self *GuestSaveImageTask) OnSaveRootImageCompleteFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { @@ -73,5 +77,13 @@ func (self *GuestSaveImageTask) OnStartServerComplete(ctx context.Context, guest } func (self *GuestSaveImageTask) OnStartServerCompleteFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + self.SetStageFailed(ctx, nil) +} + +func (self *GuestSaveImageTask) OnSyncstatusComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { self.SetStageComplete(ctx, nil) } + +func (self *GuestSaveImageTask) OnSyncstatusCompleteFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + self.SetStageFailed(ctx, nil) +} diff --git a/pkg/compute/tasks/guest_save_instance_image_task.go b/pkg/compute/tasks/guest_save_instance_image_task.go index e7daaa17f1..3941e42101 100644 --- a/pkg/compute/tasks/guest_save_instance_image_task.go +++ b/pkg/compute/tasks/guest_save_instance_image_task.go @@ -48,13 +48,15 @@ func (self *GuestSaveGuestImageTask) OnInit(ctx context.Context, obj db.IStandal for index, dataDisk := range disks.Data { params := jsonutils.DeepCopy(self.Params).(*jsonutils.JSONDict) params.Add(imageIds[index], "image_id") - if err := dataDisk.StartDiskSaveTask(ctx, self.UserCred, params, self.GetTaskId()); err != nil { + opts := api.DiskSaveInput{ImageId: imageIds[index].String()} + if err := dataDisk.StartDiskSaveTask(ctx, self.UserCred, opts, self.GetTaskId()); err != nil { self.taskFailed(ctx, guest, jsonutils.NewString(err.Error())) } } self.Params.Add(imageIds[len(imageIds)-1], "image_id") - if err := disks.Root.StartDiskSaveTask(ctx, self.UserCred, self.Params, self.GetTaskId()); err != nil { + opts := api.DiskSaveInput{ImageId: imageIds[len(imageIds)-1].String()} + if err := disks.Root.StartDiskSaveTask(ctx, self.UserCred, opts, self.GetTaskId()); err != nil { self.taskFailed(ctx, guest, jsonutils.NewString(err.Error())) } } diff --git a/pkg/multicloud/aliyun/instance.go b/pkg/multicloud/aliyun/instance.go index dc1881787d..98c519892a 100644 --- a/pkg/multicloud/aliyun/instance.go +++ b/pkg/multicloud/aliyun/instance.go @@ -1090,3 +1090,35 @@ func (self *SInstance) SetAutoRenew(autoRenew bool) error { func (self *SInstance) SetMetadata(tags map[string]string, replace bool) error { return self.host.zone.region.SetResourceTags("ecs", "instance", []string{self.InstanceId}, tags, replace) } + +func (self *SRegion) SaveImage(instanceId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) { + params := map[string]string{ + "InstanceId": instanceId, + "ImageName": opts.Name, + "Description": opts.Notes, + "ClientToken": utils.GenRequestId(20), + } + resp, err := self.ecsRequest("CreateImage", params) + if err != nil { + return nil, errors.Wrapf(err, "CreateImage") + } + ret := struct{ ImageId string }{} + err = resp.Unmarshal(&ret) + if err != nil { + return nil, errors.Wrapf(err, "resp.Unmarshal") + } + image, err := self.GetImage(ret.ImageId) + if err != nil { + return nil, errors.Wrapf(err, "GetImage(%s)", ret.ImageId) + } + image.storageCache = self.getStoragecache() + return image, nil +} + +func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) { + image, err := self.host.zone.region.SaveImage(self.InstanceId, opts) + if err != nil { + return nil, errors.Wrapf(err, "SaveImage(%s)", opts.Name) + } + return image, nil +} diff --git a/pkg/multicloud/aliyun/shell/instance.go b/pkg/multicloud/aliyun/shell/instance.go index 477af327c9..bd781ae787 100644 --- a/pkg/multicloud/aliyun/shell/instance.go +++ b/pkg/multicloud/aliyun/shell/instance.go @@ -17,6 +17,7 @@ package shell import ( "fmt" + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud/aliyun" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -212,4 +213,22 @@ func init() { return cli.SetInstanceAutoRenew(args.ID, args.AutoRenew) }) + type InstanceSaveImageOptions struct { + ID string `help:"Instance ID"` + IMAGE_NAME string `help:"Image name"` + Notes string `hlep:"Image desc"` + } + shellutils.R(&InstanceSaveImageOptions{}, "instance-save-image", "Save instance to image", func(cli *aliyun.SRegion, args *InstanceSaveImageOptions) error { + opts := cloudprovider.SaveImageOptions{ + Name: args.IMAGE_NAME, + Notes: args.Notes, + } + image, err := cli.SaveImage(args.ID, &opts) + if err != nil { + return err + } + printObject(image) + return nil + }) + } diff --git a/pkg/multicloud/aws/instance.go b/pkg/multicloud/aws/instance.go index 83eb8289fa..7af28ba5d8 100644 --- a/pkg/multicloud/aws/instance.go +++ b/pkg/multicloud/aws/instance.go @@ -1230,3 +1230,45 @@ func (self *SInstance) GetArn() string { } return fmt.Sprintf("arn:%s:ec2:%s:%s:instance/%s", partition, self.host.zone.region.GetId(), self.GetAccountId(), self.InstanceId) } + +func (self *SRegion) SaveImage(instanceId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) { + params := map[string]string{ + "Description": opts.Notes, + "InstanceId": instanceId, + "Name": opts.Name, + } + ret := struct { + ImageId string `xml:"imageId"` + }{} + err := self.ec2Request("CreateImage", params, &ret) + if err != nil { + return nil, errors.Wrapf(err, "CreateImage") + } + err = cloudprovider.Wait(time.Second*10, time.Minute*5, func() (bool, error) { + _, err := self.GetImage(ret.ImageId) + if err != nil { + if errors.Cause(err) == cloudprovider.ErrNotFound { + return false, nil + } + return false, errors.Wrapf(err, "GetImage(%s)", ret.ImageId) + } + return true, nil + }) + if err != nil { + return nil, errors.Wrapf(err, "wait for image created") + } + image, err := self.GetImage(ret.ImageId) + if err != nil { + return nil, errors.Wrapf(err, "GetImage(%s)", ret.ImageId) + } + image.storageCache = self.getStoragecache() + return image, nil +} + +func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) { + image, err := self.host.zone.region.SaveImage(self.InstanceId, opts) + if err != nil { + return nil, errors.Wrapf(err, "SaveImage") + } + return image, nil +} diff --git a/pkg/multicloud/aws/shell/instance.go b/pkg/multicloud/aws/shell/instance.go index 7ef9747b22..026b915abd 100644 --- a/pkg/multicloud/aws/shell/instance.go +++ b/pkg/multicloud/aws/shell/instance.go @@ -19,6 +19,7 @@ import ( "fmt" "io/ioutil" + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud/aws" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -177,4 +178,23 @@ func init() { } return nil }) + + type InstanceSaveImageOptions struct { + ID string `help:"Instance ID"` + IMAGE_NAME string `help:"Image name"` + Notes string `hlep:"Image desc"` + } + shellutils.R(&InstanceSaveImageOptions{}, "instance-save-image", "Save instance to image", func(cli *aws.SRegion, args *InstanceSaveImageOptions) error { + opts := cloudprovider.SaveImageOptions{ + Name: args.IMAGE_NAME, + Notes: args.Notes, + } + image, err := cli.SaveImage(args.ID, &opts) + if err != nil { + return err + } + printObject(image) + return nil + }) + } diff --git a/pkg/multicloud/azure/instance.go b/pkg/multicloud/azure/instance.go index 6ce248a7f1..f39b4e6cb6 100644 --- a/pkg/multicloud/azure/instance.go +++ b/pkg/multicloud/azure/instance.go @@ -917,3 +917,39 @@ func (self *SInstance) GetProjectId() string { func (self *SInstance) GetError() error { return nil } + +func (self *SRegion) SaveImage(osType, diskId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) { + params := map[string]interface{}{ + "Location": self.Name, + "Name": opts.Name, + "Properties": map[string]interface{}{ + "storageProfile": map[string]interface{}{ + "osDisk": map[string]interface{}{ + "osType": osType, + "managedDisk": map[string]string{ + "id": diskId, + }, + "osState": "Generalized", + }, + }, + }, + "Type": "Microsoft.Compute/images", + } + image := &SImage{storageCache: self.getStoragecache()} + err := self.create("", jsonutils.Marshal(params), image) + if err != nil { + return nil, errors.Wrapf(err, "create image") + } + return image, nil +} + +func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) { + if self.Properties.StorageProfile.OsDisk.ManagedDisk == nil { + return nil, fmt.Errorf("invalid os disk for save image") + } + image, err := self.host.zone.region.SaveImage(self.GetOSType(), self.Properties.StorageProfile.OsDisk.ManagedDisk.ID, opts) + if err != nil { + return nil, errors.Wrapf(err, "SaveImage") + } + return image, nil +} diff --git a/pkg/multicloud/azure/shell/instance.go b/pkg/multicloud/azure/shell/instance.go index 4a39406dd4..676286feb2 100644 --- a/pkg/multicloud/azure/shell/instance.go +++ b/pkg/multicloud/azure/shell/instance.go @@ -18,6 +18,7 @@ import ( "context" "fmt" + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud/azure" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -196,4 +197,24 @@ func init() { shellutils.R(&InstanceSecurityGroupOptions{}, "instance-set-secgrp", "Attach a disk to intance", func(cli *azure.SRegion, args *InstanceSecurityGroupOptions) error { return cli.SetSecurityGroup(args.ID, args.SecurityGroup) }) + + type InstanceSaveImageOptions struct { + DISK_ID string `help:"Instance Os Disk ID"` + IMAGE_NAME string `help:"Image name"` + Notes string `hlep:"Image desc"` + OsType string `help:"Os Type" choices:"Linux|Windows" default:"Linux"` + } + shellutils.R(&InstanceSaveImageOptions{}, "instance-save-image", "Save instance to image", func(cli *azure.SRegion, args *InstanceSaveImageOptions) error { + opts := cloudprovider.SaveImageOptions{ + Name: args.IMAGE_NAME, + Notes: args.Notes, + } + image, err := cli.SaveImage(args.OsType, args.DISK_ID, &opts) + if err != nil { + return err + } + printObject(image) + return nil + }) + } diff --git a/pkg/multicloud/google/instance.go b/pkg/multicloud/google/instance.go index 520f6a1dbe..646a2665ee 100644 --- a/pkg/multicloud/google/instance.go +++ b/pkg/multicloud/google/instance.go @@ -878,3 +878,31 @@ func (region *SRegion) RebuildRoot(instanceId string, imageId string, sysDiskSiz } return disk.GetGlobalId(), nil } + +func (self *SRegion) SaveImage(diskId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) { + params := map[string]interface{}{ + "name": opts.Name, + "description": opts.Notes, + "sourceDisk": diskId, + } + image := &SImage{} + err := self.Insert("global/images", jsonutils.Marshal(params), image) + if err != nil { + return nil, errors.Wrapf(err, "Insert") + } + image.storagecache = self.getStoragecache() + return image, nil +} + +func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) { + for i := range self.Disks { + if self.Disks[0].Index == 0 { + image, err := self.host.zone.region.SaveImage(self.Disks[i].Source, opts) + if err != nil { + return nil, errors.Wrapf(err, "SaveImage") + } + return image, nil + } + } + return nil, errors.Wrapf(cloudprovider.ErrNotFound, "no valid system disk found") +} diff --git a/pkg/multicloud/google/shell/instance.go b/pkg/multicloud/google/shell/instance.go index 3a6caacdaf..d44fc80b54 100644 --- a/pkg/multicloud/google/shell/instance.go +++ b/pkg/multicloud/google/shell/instance.go @@ -19,6 +19,7 @@ import ( "yunion.io/x/pkg/errors" + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud/google" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -184,4 +185,22 @@ func init() { return nil }) + type InstanceSaveImageOptions struct { + DISK_ID string `help:"Instance System disk ID"` + IMAGE_NAME string `help:"Image name"` + Notes string `hlep:"Image desc"` + } + shellutils.R(&InstanceSaveImageOptions{}, "instance-save-image", "Save instance to image", func(cli *google.SRegion, args *InstanceSaveImageOptions) error { + opts := cloudprovider.SaveImageOptions{ + Name: args.IMAGE_NAME, + Notes: args.Notes, + } + image, err := cli.SaveImage(args.DISK_ID, &opts) + if err != nil { + return err + } + printObject(image) + return nil + }) + } diff --git a/pkg/multicloud/google/storagecache.go b/pkg/multicloud/google/storagecache.go index 69bce663c2..a134bef76d 100644 --- a/pkg/multicloud/google/storagecache.go +++ b/pkg/multicloud/google/storagecache.go @@ -213,7 +213,7 @@ func (region *SRegion) GetIStoragecaches() ([]cloudprovider.ICloudStoragecache, return []cloudprovider.ICloudStoragecache{cache}, nil } -func (region *SRegion) getStoragecache() cloudprovider.ICloudStoragecache { +func (region *SRegion) getStoragecache() *SStoragecache { return &SStoragecache{region: region} } diff --git a/pkg/multicloud/huawei/instance.go b/pkg/multicloud/huawei/instance.go index 6e4fbbe6a0..59c29587c0 100644 --- a/pkg/multicloud/huawei/instance.go +++ b/pkg/multicloud/huawei/instance.go @@ -1417,3 +1417,51 @@ func updateWindowsUserData(userData string, osVersion string, username, password return base64.StdEncoding.EncodeToString([]byte(shells)), nil } + +func (self *SRegion) SaveImage(instanceId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) { + params := map[string]string{ + "name": opts.Name, + "instance_id": instanceId, + } + if len(opts.Notes) > 0 { + params["description"] = func() string { + opts.Notes = strings.ReplaceAll(opts.Notes, "<", "") + opts.Notes = strings.ReplaceAll(opts.Notes, ">", "") + opts.Notes = strings.ReplaceAll(opts.Notes, "\n", "") + if len(opts.Notes) > 1024 { + opts.Notes = opts.Notes[:1024] + } + return opts.Notes + }() + } + resp, err := self.ecsClient.Images.CreateInContextWithSpec(nil, "action", jsonutils.Marshal(params), "") + if err != nil { + return nil, errors.Wrapf(err, "Images.Create") + } + jobId, err := resp.GetString("job_id") + if err != nil { + return nil, errors.Wrapf(err, "resp.GetString(job_id)") + } + err = self.waitTaskStatus(self.ecsClient.Images.ServiceType(), jobId, TASK_SUCCESS, 15*time.Second, 10*time.Minute) + if err != nil { + return nil, errors.Wrapf(err, "waitTaskStatus") + } + imageId, err := self.GetTaskEntityID(self.ecsClient.Images.ServiceType(), jobId, "image_id") + if err != nil { + return nil, errors.Wrapf(err, "GetTaskEntityID") + } + image, err := self.GetImage(imageId) + if err != nil { + return nil, errors.Wrapf(err, "GetImage(%s)", imageId) + } + image.storageCache = self.getStoragecache() + return image, nil +} + +func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) { + image, err := self.host.zone.region.SaveImage(self.ID, opts) + if err != nil { + return nil, errors.Wrapf(err, "SaveImage") + } + return image, nil +} diff --git a/pkg/multicloud/huawei/shell/instance.go b/pkg/multicloud/huawei/shell/instance.go index cef83398c9..d6700e74a4 100644 --- a/pkg/multicloud/huawei/shell/instance.go +++ b/pkg/multicloud/huawei/shell/instance.go @@ -18,6 +18,7 @@ import ( "context" "fmt" + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud/huawei" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -170,4 +171,23 @@ func init() { } return nil }) + + type InstanceSaveImageOptions struct { + ID string `help:"Instance ID"` + IMAGE_NAME string `help:"Image name"` + Notes string `hlep:"Image desc"` + } + shellutils.R(&InstanceSaveImageOptions{}, "instance-save-image", "Save instance to image", func(cli *huawei.SRegion, args *InstanceSaveImageOptions) error { + opts := cloudprovider.SaveImageOptions{ + Name: args.IMAGE_NAME, + Notes: args.Notes, + } + image, err := cli.SaveImage(args.ID, &opts) + if err != nil { + return err + } + printObject(image) + return nil + }) + } diff --git a/pkg/multicloud/instance_base.go b/pkg/multicloud/instance_base.go index 1eb06a6c4f..5d2c7cf5e2 100644 --- a/pkg/multicloud/instance_base.go +++ b/pkg/multicloud/instance_base.go @@ -17,6 +17,8 @@ package multicloud import ( "context" + "yunion.io/x/pkg/errors" + "yunion.io/x/onecloud/pkg/cloudprovider" ) @@ -64,3 +66,7 @@ func (instance *SInstanceBase) CreateInstanceSnapshot(ctx context.Context, name func (instance *SInstanceBase) ResetToInstanceSnapshot(ctx context.Context, idStr string) error { return cloudprovider.ErrNotImplemented } + +func (self *SInstanceBase) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) { + return nil, errors.Wrapf(cloudprovider.ErrNotImplemented, "SaveImage") +} diff --git a/pkg/multicloud/qcloud/image.go b/pkg/multicloud/qcloud/image.go index 091cd35727..0220e26e24 100644 --- a/pkg/multicloud/qcloud/image.go +++ b/pkg/multicloud/qcloud/image.go @@ -74,27 +74,29 @@ func (self *SRegion) GetImages(status string, owner string, imageIds []string, n params["Limit"] = fmt.Sprintf("%d", limit) params["Offset"] = fmt.Sprintf("%d", offset) - filter := 0 - if len(status) > 0 { - params[fmt.Sprintf("Filters.%d.Name", filter)] = "image-state" - params[fmt.Sprintf("Filters.%d.Values.0", filter)] = status - filter++ - } - if imageIds != nil && len(imageIds) > 0 { - for index, imageId := range imageIds { - params[fmt.Sprintf("ImageIds.%d", index)] = imageId - } - } - if len(owner) > 0 { - params[fmt.Sprintf("Filters.%d.Name", filter)] = "image-type" - params[fmt.Sprintf("Filters.%d.Values.0", filter)] = owner - filter++ + for index, imageId := range imageIds { + params[fmt.Sprintf("ImageIds.%d", index)] = imageId } - if len(name) > 0 { - params[fmt.Sprintf("Filters.%d.Name", filter)] = "image-name" - params[fmt.Sprintf("Filters.%d.Values.0", filter)] = name - filter++ + if len(imageIds) == 0 { // imageIds 不能和Filter同时查询 + filter := 0 + if len(status) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "image-state" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = status + filter++ + } + + if len(owner) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "image-type" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = owner + filter++ + } + + if len(name) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "image-name" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = name + filter++ + } } images := make([]SImage, 0) diff --git a/pkg/multicloud/qcloud/instance.go b/pkg/multicloud/qcloud/instance.go index af35e81721..cd51f1355e 100644 --- a/pkg/multicloud/qcloud/instance.go +++ b/pkg/multicloud/qcloud/instance.go @@ -1015,3 +1015,46 @@ func (self *SInstance) SetAutoRenew(autoRenew bool) error { func (self *SInstance) SetMetadata(tags map[string]string, replace bool) error { return self.host.zone.region.SetResourceTags("cvm", "instance", []string{self.InstanceId}, tags, replace) } + +func (self *SRegion) SaveImage(instanceId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) { + params := map[string]string{ + "ImageName": opts.Name, + "InstanceId": instanceId, + } + if len(opts.Notes) > 0 { + params["ImageDescription"] = opts.Notes + } + resp, err := self.cvmRequest("CreateImage", params, true) + if err != nil { + return nil, errors.Wrapf(err, "CreateImage") + } + ret := struct{ ImageId string }{} + err = resp.Unmarshal(&ret) + if err != nil { + return nil, errors.Wrapf(err, "resp.Unmarshal") + } + imageIds := []string{} + if len(ret.ImageId) > 0 { + imageIds = append(imageIds, ret.ImageId) + } + + images, _, err := self.GetImages("", "PRIVATE_IMAGE", imageIds, opts.Name, 0, 1) + if err != nil { + return nil, errors.Wrapf(err, "GetImage(%s,%s)", opts.Name, ret.ImageId) + } + for i := range images { + if images[i].ImageId == ret.ImageId || images[i].ImageName == opts.Name { + images[i].storageCache = self.getStoragecache() + return &images[i], nil + } + } + return nil, errors.Wrapf(cloudprovider.ErrNotFound, "after save image %s", opts.Name) +} + +func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) { + image, err := self.host.zone.region.SaveImage(self.InstanceId, opts) + if err != nil { + return nil, errors.Wrapf(err, "SaveImage") + } + return image, nil +} diff --git a/pkg/multicloud/qcloud/shell/instance.go b/pkg/multicloud/qcloud/shell/instance.go index b36d1080ac..9b11ce854e 100644 --- a/pkg/multicloud/qcloud/shell/instance.go +++ b/pkg/multicloud/qcloud/shell/instance.go @@ -223,4 +223,22 @@ func init() { return cli.SetInstanceAutoRenew(args.ID, args.AutoRenew) }) + type InstanceSaveImageOptions struct { + ID string `help:"Instance ID"` + IMAGE_NAME string `help:"Image name"` + Notes string `hlep:"Image desc"` + } + shellutils.R(&InstanceSaveImageOptions{}, "instance-save-image", "Save instance to image", func(cli *qcloud.SRegion, args *InstanceSaveImageOptions) error { + opts := cloudprovider.SaveImageOptions{ + Name: args.IMAGE_NAME, + Notes: args.Notes, + } + image, err := cli.SaveImage(args.ID, &opts) + if err != nil { + return err + } + printObject(image) + return nil + }) + }