Skip to content

Commit

Permalink
Merge pull request #20023 from zexi/container-save-vm-image
Browse files Browse the repository at this point in the history
feat(container): save volume_mount to image
  • Loading branch information
zexi authored Apr 16, 2024
2 parents e3d0e59 + 6c3079d commit ff9bbff
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 53 deletions.
1 change: 1 addition & 0 deletions cmd/climc/shell/compute/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down
11 changes: 9 additions & 2 deletions pkg/apis/compute/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"`
}
7 changes: 7 additions & 0 deletions pkg/apis/host/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
4 changes: 4 additions & 0 deletions pkg/compute/guestdrivers/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
63 changes: 63 additions & 0 deletions pkg/compute/models/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
102 changes: 57 additions & 45 deletions pkg/compute/models/disks.go
Original file line number Diff line number Diff line change
Expand Up @@ -1149,34 +1149,67 @@ func (self *SDisk) GetZone() (*SZone, error) {
return storage.getZone()
}

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 {
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": input.Name, "admin": "true"}))
imageList, err := image.Images.List(s, jsonutils.Marshal(map[string]string{"name": name, "admin": "true"}))
if err != nil {
return "", err
return err
}
if imageList.Total > 0 {
return "", httperrors.NewConflictError("Duplicate image name %s", input.Name)
return httperrors.NewConflictError("Duplicate image name %s", name)
}
}
return nil
}

opts := struct {
Name string
GenerateName string
VirtualSize int
DiskFormat string
OsArch string
Properties map[string]string
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
}

ProjectId 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(&quota)); 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
}

EncryptKeyId string
}{
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")
}
imageInput := &CreateGlanceImageInput{
Name: input.Name,
GenerateName: input.GenerateName,
VirtualSize: self.DiskSize,
Expand All @@ -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(&quota)); 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) {
Expand All @@ -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")
Expand Down
1 change: 1 addition & 0 deletions pkg/compute/models/pod_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
49 changes: 49 additions & 0 deletions pkg/compute/tasks/container_save_volume_mount_image_task.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit ff9bbff

Please sign in to comment.