diff --git a/cmd/climc/shell/compute/containers.go b/cmd/climc/shell/compute/containers.go index f6d2dd6272..b3bc031047 100644 --- a/cmd/climc/shell/compute/containers.go +++ b/cmd/climc/shell/compute/containers.go @@ -41,6 +41,7 @@ func init() { cmd.BatchPerform("stop", new(options.ContainerStopOptions)) cmd.BatchPerform("start", new(options.ContainerStartOptions)) cmd.BatchPerform("syncstatus", new(options.ContainerIdsOptions)) + cmd.Perform("save-volume-mount-image", new(options.ContainerSaveVolumeMountImage)) type UpdateSpecOptions struct { ID string `help:"ID or name of server" json:"-"` diff --git a/pkg/apis/compute/container.go b/pkg/apis/compute/container.go index 7c86652776..d1f686bfa0 100644 --- a/pkg/apis/compute/container.go +++ b/pkg/apis/compute/container.go @@ -47,8 +47,8 @@ const ( CONTAINER_STATUS_PULLED_IMAGE = "pulled_image" CONTAINER_STATUS_CREATING = "creating" CONTAINER_STATUS_CREATE_FAILED = "create_failed" - CONTAINER_STATUS_CACHING_IMAGE = "caching_image" - CONTAINER_STATUS_CACHE_IMAGE_FAILED = "cache_image_failed" + CONTAINER_STATUS_SAVING_IMAGE = "saving_image" + CONTAINER_STATUS_SAVE_IMAGE_FAILED = "save_image_failed" CONTAINER_STATUS_STARTING = "starting" CONTAINER_STATUS_START_FAILED = "start_failed" CONTAINER_STATUS_STOPPING = "stopping" @@ -134,3 +134,10 @@ type ContainerDevice struct { IsolatedDevice *ContainerIsolatedDevice `json:"isolated_device"` Host *ContainerHostDevice `json:"host"` } + +type ContainerSaveVolumeMountToImageInput struct { + Name string `json:"name"` + GenerateName string `json:"generate_name"` + Notes string `json:"notes"` + Index int `json:"index"` +} diff --git a/pkg/apis/host/container.go b/pkg/apis/host/container.go index d3e1dc23ea..673cd6fe9f 100644 --- a/pkg/apis/host/container.go +++ b/pkg/apis/host/container.go @@ -102,3 +102,10 @@ type ContainerDesc struct { Name string `json:"name"` Spec *ContainerSpec `json:"spec"` } + +type ContainerSaveVolumeMountToImageInput struct { + ImageId string `json:"image_id"` + + VolumeMountIndex int `json:"volume_mount_index"` + VolumeMount *ContainerVolumeMount `json:"volume_mount"` +} diff --git a/pkg/compute/guestdrivers/pod.go b/pkg/compute/guestdrivers/pod.go index 630a20ba94..3b86d284ac 100644 --- a/pkg/compute/guestdrivers/pod.go +++ b/pkg/compute/guestdrivers/pod.go @@ -477,3 +477,7 @@ func (p *SPodDriver) IsSupportFloppy(guest *models.SGuest) (bool, error) { func (p *SPodDriver) GetChangeConfigStatus(guest *models.SGuest) ([]string, error) { return []string{api.VM_READY}, nil } + +func (p *SPodDriver) RequestSaveVolumeMountImage(ctx context.Context, userCred mcclient.TokenCredential, task models.IContainerTask) error { + return p.performContainerAction(ctx, userCred, task, "save-volume-mount-to-image", task.GetParams()) +} diff --git a/pkg/compute/models/containers.go b/pkg/compute/models/containers.go index 40181e37b7..bbd9643ec9 100644 --- a/pkg/compute/models/containers.go +++ b/pkg/compute/models/containers.go @@ -26,6 +26,7 @@ import ( "yunion.io/x/onecloud/pkg/apis" api "yunion.io/x/onecloud/pkg/apis/compute" hostapi "yunion.io/x/onecloud/pkg/apis/host" + imageapi "yunion.io/x/onecloud/pkg/apis/image" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/httperrors" @@ -463,3 +464,65 @@ func (c *SContainer) GetJsonDescAtHost() (*hostapi.ContainerDesc, error) { Spec: spec, }, nil } + +func (c *SContainer) PrepareSaveImage(ctx context.Context, userCred mcclient.TokenCredential, input *api.ContainerSaveVolumeMountToImageInput) (string, error) { + imageInput := &CreateGlanceImageInput{ + Name: input.Name, + GenerateName: input.GenerateName, + DiskFormat: imageapi.IMAGE_DISK_FORMAT_TGZ, + Properties: map[string]string{ + "notes": input.Notes, + }, + // inherit the ownership of disk + ProjectId: c.ProjectId, + } + // check class metadata + cm, err := c.GetAllClassMetadata() + if err != nil { + return "", errors.Wrap(err, "unable to GetAllClassMetadata") + } + imageInput.ClassMetadata = cm + return DiskManager.CreateGlanceImage(ctx, userCred, imageInput) +} + +func (c *SContainer) PerformSaveVolumeMountImage(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input *api.ContainerSaveVolumeMountToImageInput) (*hostapi.ContainerSaveVolumeMountToImageInput, error) { + if c.GetStatus() != api.CONTAINER_STATUS_EXITED { + return nil, httperrors.NewInvalidStatusError("Can't save volume disk of container in status %s", c.Status) + } + if c.GetPod().GetStatus() != api.VM_READY { + return nil, httperrors.NewInvalidStatusError("Can't save volume disk of pod in status %s", c.GetPod().GetStatus()) + } + vols := c.GetVolumeMounts() + if input.Index < 0 || input.Index >= len(vols) { + return nil, httperrors.NewInputParameterError("Only %d volume_mounts", len(vols)) + } + + imageId, err := c.PrepareSaveImage(ctx, userCred, input) + if err != nil { + return nil, errors.Wrap(err, "prepare to save image") + } + vrs, err := c.GetVolumeMountRelations() + if err != nil { + return nil, errors.Wrap(err, "GetVolumeMountRelations") + } + hvm, err := vrs[input.Index].ToHostMount() + if err != nil { + return nil, errors.Wrap(err, "ToHostMount") + } + hostInput := &hostapi.ContainerSaveVolumeMountToImageInput{ + ImageId: imageId, + VolumeMountIndex: input.Index, + VolumeMount: hvm, + } + + return hostInput, c.StartSaveVolumeMountImage(ctx, userCred, hostInput, "") +} + +func (c *SContainer) StartSaveVolumeMountImage(ctx context.Context, userCred mcclient.TokenCredential, input *hostapi.ContainerSaveVolumeMountToImageInput, parentTaskId string) error { + c.SetStatus(ctx, userCred, api.CONTAINER_STATUS_SAVING_IMAGE, "") + task, err := taskman.TaskManager.NewTask(ctx, "ContainerSaveVolumeMountImageTask", c, userCred, jsonutils.Marshal(input).(*jsonutils.JSONDict), parentTaskId, "", nil) + if err != nil { + return errors.Wrap(err, "NewTask") + } + return task.ScheduleRun(nil) +} diff --git a/pkg/compute/models/disks.go b/pkg/compute/models/disks.go index 80c869d428..ab4f2f9d77 100644 --- a/pkg/compute/models/disks.go +++ b/pkg/compute/models/disks.go @@ -1149,34 +1149,67 @@ func (self *SDisk) GetZone() (*SZone, error) { return storage.getZone() } +func (m *SDiskManager) CheckGlanceImage(ctx context.Context, userCred mcclient.TokenCredential, name string, generateName string) error { + if len(generateName) == 0 { + s := auth.GetAdminSession(ctx, options.Options.Region) + imageList, err := image.Images.List(s, jsonutils.Marshal(map[string]string{"name": name, "admin": "true"})) + if err != nil { + return err + } + if imageList.Total > 0 { + return httperrors.NewConflictError("Duplicate image name %s", name) + } + } + return nil +} + +type CreateGlanceImageInput struct { + Name string + GenerateName string + VirtualSize int + DiskFormat string + OsArch string + Properties map[string]string + ProjectId string + EncryptKeyId string + ClassMetadata map[string]string +} + +func (m *SDiskManager) CreateGlanceImage(ctx context.Context, userCred mcclient.TokenCredential, input *CreateGlanceImageInput) (string, error) { + if err := DiskManager.CheckGlanceImage(ctx, userCred, input.Name, input.GenerateName); err != nil { + return "", err + } + /* + no need to check quota anymore + session := auth.GetSession(userCred, options.Options.Region, "v2") + quota := image_models.SQuota{Image: 1} + if _, err := image.ImageQuotas.DoQuotaCheck(session, jsonutils.Marshal("a)); err != nil { + return "", err + }*/ + us := auth.GetSession(ctx, userCred, options.Options.Region) + result, err := image.Images.Create(us, jsonutils.Marshal(input)) + if err != nil { + return "", err + } + imageId, err := result.GetString("id") + if err != nil { + return "", err + } + if len(input.ClassMetadata) > 0 { + _, err = image.Images.PerformAction(us, imageId, "set-class-metadata", jsonutils.Marshal(input.ClassMetadata)) + if err != nil { + return "", errors.Wrapf(err, "unable to SetClassMetadata for image %s", imageId) + } + } + return imageId, 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") } - if len(input.GenerateName) == 0 { - s := auth.GetAdminSession(ctx, options.Options.Region) - imageList, err := image.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", input.Name) - } - } - - opts := struct { - Name string - GenerateName string - VirtualSize int - DiskFormat string - OsArch string - Properties map[string]string - - ProjectId string - - EncryptKeyId string - }{ + imageInput := &CreateGlanceImageInput{ Name: input.Name, GenerateName: input.GenerateName, VirtualSize: self.DiskSize, @@ -1191,43 +1224,21 @@ func (self *SDisk) PrepareSaveImage(ctx context.Context, userCred mcclient.Token // inherit the ownership of disk ProjectId: self.ProjectId, } - if self.IsEncrypted() { encKey, err := self.GetEncryptInfo(ctx, userCred) if err != nil { return "", errors.Wrap(err, "GetEncryptInfo") } - opts.EncryptKeyId = encKey.Id - } - - /* - no need to check quota anymore - session := auth.GetSession(userCred, options.Options.Region, "v2") - quota := image_models.SQuota{Image: 1} - if _, err := image.ImageQuotas.DoQuotaCheck(session, jsonutils.Marshal("a)); err != nil { - return "", err - }*/ - us := auth.GetSession(ctx, userCred, options.Options.Region) - result, err := image.Images.Create(us, jsonutils.Marshal(opts)) - if err != nil { - return "", err - } - imageId, err := result.GetString("id") - if err != nil { - return "", err + imageInput.EncryptKeyId = encKey.Id } // check class metadata cm, err := self.GetAllClassMetadata() if err != nil { return "", errors.Wrap(err, "unable to GetAllClassMetadata") } - if len(cm) > 0 { - _, err = image.Images.PerformAction(us, imageId, "set-class-metadata", jsonutils.Marshal(cm)) - if err != nil { - return "", errors.Wrapf(err, "unable to SetClassMetadata for image %s", imageId) - } - } - return imageId, nil + imageInput.ClassMetadata = cm + + return DiskManager.CreateGlanceImage(ctx, userCred, imageInput) } func (self *SDisk) PerformSave(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.DiskSaveInput) (jsonutils.JSONObject, error) { @@ -1249,6 +1260,7 @@ func (self *SDisk) PerformSave(ctx context.Context, userCred mcclient.TokenCrede opts := api.ServerSaveImageInput{ Name: input.Name, } + input.ImageId, err = self.PrepareSaveImage(ctx, userCred, opts) if err != nil { return nil, errors.Wrapf(err, "PrepareSaveImage") diff --git a/pkg/compute/models/pod_driver.go b/pkg/compute/models/pod_driver.go index 6bff2aa509..43a32d1501 100644 --- a/pkg/compute/models/pod_driver.go +++ b/pkg/compute/models/pod_driver.go @@ -23,4 +23,5 @@ type IPodDriver interface { RequestDeleteContainer(ctx context.Context, userCred mcclient.TokenCredential, task IContainerTask) error RequestSyncContainerStatus(ctx context.Context, userCred mcclient.TokenCredential, task IContainerTask) error RequestPullContainerImage(ctx context.Context, userCred mcclient.TokenCredential, task IContainerTask) error + RequestSaveVolumeMountImage(ctx context.Context, userCred mcclient.TokenCredential, task IContainerTask) error } diff --git a/pkg/compute/tasks/container_save_volume_mount_image_task.go b/pkg/compute/tasks/container_save_volume_mount_image_task.go new file mode 100644 index 0000000000..a64b9c1195 --- /dev/null +++ b/pkg/compute/tasks/container_save_volume_mount_image_task.go @@ -0,0 +1,49 @@ +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" +) + +func init() { + taskman.RegisterTask(ContainerSaveVolumeMountImageTask{}) +} + +type ContainerSaveVolumeMountImageTask struct { + ContainerBaseTask +} + +func (t *ContainerSaveVolumeMountImageTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) { + t.requestSaveImage(ctx, obj.(*models.SContainer)) +} + +func (t *ContainerSaveVolumeMountImageTask) requestSaveImage(ctx context.Context, container *models.SContainer) { + t.SetStage("OnImageSaved", nil) + if err := t.GetPodDriver().RequestSaveVolumeMountImage(ctx, t.GetUserCred(), t); err != nil { + t.OnImageSavedFailed(ctx, container, jsonutils.NewString(err.Error())) + return + } +} + +func (t *ContainerSaveVolumeMountImageTask) OnImageSaved(ctx context.Context, container *models.SContainer, data jsonutils.JSONObject) { + t.SetStage("OnSyncStatus", nil) + container.StartSyncStatusTask(ctx, t.GetUserCred(), t.GetTaskId()) +} + +func (t *ContainerSaveVolumeMountImageTask) OnImageSavedFailed(ctx context.Context, container *models.SContainer, reason jsonutils.JSONObject) { + container.SetStatus(ctx, t.GetUserCred(), api.CONTAINER_STATUS_SAVE_IMAGE_FAILED, reason.String()) + t.SetStageFailed(ctx, reason) +} + +func (t *ContainerSaveVolumeMountImageTask) OnSyncStatus(ctx context.Context, container *models.SContainer, data jsonutils.JSONObject) { + t.SetStageComplete(ctx, nil) +} +func (t *ContainerSaveVolumeMountImageTask) OnSyncStatusFailed(ctx context.Context, container *models.SContainer, reason jsonutils.JSONObject) { + t.SetStageFailed(ctx, reason) +} diff --git a/pkg/hostman/guestman/pod.go b/pkg/hostman/guestman/pod.go index 68176389d4..ada12cc15e 100644 --- a/pkg/hostman/guestman/pod.go +++ b/pkg/hostman/guestman/pod.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "io/ioutil" + "os" "path" "path/filepath" "strings" @@ -46,9 +47,11 @@ import ( "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/auth" computemod "yunion.io/x/onecloud/pkg/mcclient/modules/compute" + "yunion.io/x/onecloud/pkg/mcclient/modules/image" "yunion.io/x/onecloud/pkg/util/fileutils2" "yunion.io/x/onecloud/pkg/util/netutils2/getport" "yunion.io/x/onecloud/pkg/util/pod" + "yunion.io/x/onecloud/pkg/util/procutils" ) type PodInstance interface { @@ -60,6 +63,7 @@ type PodInstance interface { SyncContainerStatus(ctx context.Context, cred mcclient.TokenCredential, ctrId string) (jsonutils.JSONObject, error) StopContainer(ctx context.Context, userCred mcclient.TokenCredential, ctrId string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) PullImage(ctx context.Context, userCred mcclient.TokenCredential, ctrId string, input *hostapi.ContainerPullImageInput) (jsonutils.JSONObject, error) + SaveVolumeMountToImage(ctx context.Context, userCred mcclient.TokenCredential, input *hostapi.ContainerSaveVolumeMountToImageInput, ctrId string) (jsonutils.JSONObject, error) } type sContainer struct { @@ -1133,3 +1137,70 @@ func (s *sPodGuestInstance) PullImage(ctx context.Context, userCred mcclient.Tok } return jsonutils.Marshal(resp), nil } + +func (s *sPodGuestInstance) SaveVolumeMountToImage(ctx context.Context, userCred mcclient.TokenCredential, input *hostapi.ContainerSaveVolumeMountToImageInput, ctrId string) (jsonutils.JSONObject, error) { + vol := input.VolumeMount + drv := volume_mount.GetDriver(vol.Type) + if err := drv.Mount(s, ctrId, vol); err != nil { + return nil, errors.Wrapf(err, "mount volume %s, ctrId %s", jsonutils.Marshal(vol), ctrId) + } + defer func() { + if err := drv.Unmount(s, ctrId, vol); err != nil { + log.Warningf("unmount volume %s: %v", jsonutils.Marshal(vol), err) + } + }() + + hostPath, err := drv.GetRuntimeMountHostPath(s, ctrId, vol) + if err != nil { + return nil, errors.Wrapf(err, "get runtime host mount path of %s", jsonutils.Marshal(vol)) + } + // 1. tar hostPath to tgz + imgPath, err := s.tarGzDir(input, ctrId, hostPath) + if err != nil { + return nil, errors.Wrapf(err, "tar and zip directory %s", hostPath) + } + defer func() { + out, err := procutils.NewRemoteCommandAsFarAsPossible("rm", "-f", imgPath).Output() + if err != nil { + log.Warningf("rm -f %s: %s", imgPath, out) + } + }() + + // 2. upload target tgz to glance + if err := s.saveTarGzToGlance(ctx, input, imgPath); err != nil { + return nil, errors.Wrapf(err, "saveTarGzToGlance: %s", imgPath) + } + return nil, nil +} + +func (s *sPodGuestInstance) tarGzDir(input *hostapi.ContainerSaveVolumeMountToImageInput, ctrId string, hostPath string) (string, error) { + fp := fmt.Sprintf("volimg-%s-ctr-%s-%d.tar.gz", input.ImageId, ctrId, input.VolumeMountIndex) + outputFp := filepath.Join(s.GetVolumesDir(), fp) + cmd := fmt.Sprintf("tar -czf %s -C %s .", outputFp, hostPath) + if out, err := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", cmd).Output(); err != nil { + return "", errors.Wrapf(err, "%s: %s", cmd, out) + } + return outputFp, nil +} + +func (s *sPodGuestInstance) saveTarGzToGlance(ctx context.Context, input *hostapi.ContainerSaveVolumeMountToImageInput, imgPath string) error { + f, err := os.Open(imgPath) + if err != nil { + return err + } + defer f.Close() + finfo, err := f.Stat() + if err != nil { + return err + } + size := finfo.Size() + + var params = jsonutils.NewDict() + params.Set("image_id", jsonutils.NewString(input.ImageId)) + + if _, err := image.Images.Upload(hostutils.GetImageSession(ctx), params, f, size); err != nil { + return errors.Wrap(err, "upload image") + } + + return err +} diff --git a/pkg/hostman/guestman/podhandlers/podhandlers.go b/pkg/hostman/guestman/podhandlers/podhandlers.go index d0206599a3..29c0c60786 100644 --- a/pkg/hostman/guestman/podhandlers/podhandlers.go +++ b/pkg/hostman/guestman/podhandlers/podhandlers.go @@ -64,12 +64,13 @@ func containerActionHandler(cf containerActionFunc) appsrv.FilterHandler { func AddPodHandlers(prefix string, app *appsrv.Application) { ctrHandlers := map[string]containerActionFunc{ - "create": createContainer, - "start": startContainer, - "stop": stopContainer, - "delete": deleteContainer, - "sync-status": syncContainerStatus, - "pull-image": pullImage, + "create": createContainer, + "start": startContainer, + "stop": stopContainer, + "delete": deleteContainer, + "sync-status": syncContainerStatus, + "pull-image": pullImage, + "save-volume-mount-to-image": saveVolumeMountToImage, } for action, f := range ctrHandlers { app.AddHandler("POST", @@ -113,3 +114,11 @@ func deleteContainer(ctx context.Context, userCred mcclient.TokenCredential, pod func syncContainerStatus(ctx context.Context, userCred mcclient.TokenCredential, pod guestman.PodInstance, id string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { return pod.SyncContainerStatus(ctx, userCred, id) } + +func saveVolumeMountToImage(ctx context.Context, userCred mcclient.TokenCredential, pod guestman.PodInstance, ctrId string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { + input := new(hostapi.ContainerSaveVolumeMountToImageInput) + if err := body.Unmarshal(input); err != nil { + return nil, errors.Wrap(err, "unmarshal to input") + } + return pod.SaveVolumeMountToImage(ctx, userCred, input, ctrId) +} diff --git a/pkg/mcclient/options/compute/containers.go b/pkg/mcclient/options/compute/containers.go index 252c47da37..98f2a6c457 100644 --- a/pkg/mcclient/options/compute/containers.go +++ b/pkg/mcclient/options/compute/containers.go @@ -246,3 +246,20 @@ func (o *ContainerStopOptions) Params() (jsonutils.JSONObject, error) { type ContainerStartOptions struct { ContainerIdsOptions } + +type ContainerSaveVolumeMountImage struct { + options.ResourceIdOptions + IMAGENAME string `help:"Image name"` + INDEX int `help:"Index of volume mount"` + GenerateName string `help:"Generate image name automatically"` + Notes string `help:"Extra notes of the image"` +} + +func (o ContainerSaveVolumeMountImage) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(&computeapi.ContainerSaveVolumeMountToImageInput{ + Name: o.IMAGENAME, + GenerateName: o.GenerateName, + Notes: o.Notes, + Index: o.INDEX, + }), nil +}