Merge pull request #20023 from zexi/container-save-vm-image

feat(container): save volume_mount to image
This commit is contained in:
Zexi Li
2024-04-16 18:29:55 +08:00
committed by GitHub
11 changed files with 298 additions and 57 deletions
+1
View File
@@ -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:"-"`
+9 -2
View File
@@ -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"`
}
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+61 -49
View File
@@ -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(&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
}
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(&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) {
@@ -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")
+1
View File
@@ -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
}
@@ -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)
}
+71
View File
@@ -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
}
@@ -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)
}
@@ -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
}