feature: guest image create from vm, delete, update, list; create vm

from guest image.
This commit is contained in:
Rain
2019-10-22 17:35:32 +08:00
parent 70763615f2
commit a8fddb66cc
30 changed files with 1409 additions and 37 deletions
+410
View File
@@ -0,0 +1,410 @@
// 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 models
import (
"context"
"fmt"
"sort"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/tristate"
"yunion.io/x/pkg/utils"
api "yunion.io/x/onecloud/pkg/apis/image"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/image/options"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/rbacutils"
)
type SGuestImageManager struct {
db.SSharableVirtualResourceBaseManager
}
var GuestImageManager *SGuestImageManager
func init() {
GuestImageManager = &SGuestImageManager{
db.NewSharableVirtualResourceBaseManager(
SGuestImage{},
"guestimages_tbl",
"guestimage",
"guestimages",
),
}
GuestImageManager.SetVirtualObject(GuestImageManager)
}
type SGuestImage struct {
db.SSharableVirtualResourceBase
Protected tristate.TriState `nullable:"false" default:"true" list:"user" get:"user" create:"optional" update:"user"`
}
func (manager *SGuestImageManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
if !data.Contains("image_number") {
return nil, httperrors.NewMissingParameterError("image_number")
}
imageNum, _ := data.Int("image_number")
pendingUsage := SQuota{Image: int(imageNum)}
if err := QuotaManager.CheckSetPendingQuota(ctx, userCred, rbacutils.ScopeProject, userCred, nil,
&pendingUsage); err != nil {
return nil, httperrors.NewOutOfQuotaError("%s", err)
}
return data, nil
}
func (gi *SGuestImage) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
err := gi.SVirtualResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
if err != nil {
return err
}
gi.Status = api.IMAGE_STATUS_QUEUED
return nil
}
func (gi *SGuestImage) PostCreate(ctx context.Context, userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
kwargs := data.(*jsonutils.JSONDict)
// deal public params
kwargs.Remove("size")
kwargs.Remove("image_number")
kwargs.Remove("name")
if !kwargs.Contains("images") {
return
}
images, _ := kwargs.GetArray("images")
kwargs.Remove("images")
kwargs.Add(jsonutils.NewString(gi.Id), "guest_image_id")
imageIds := jsonutils.NewArray()
suc := true
// HACK
appParams := appsrv.AppContextGetParams(ctx)
appParams.Request.ContentLength = 0
kwargs.Add(jsonutils.NewString("此镜像为主机镜像子镜像,不可操作"), "description")
for i := 0; i < len(images); i++ {
params := jsonutils.DeepCopy(kwargs).(*jsonutils.JSONDict)
image := images[i].(*jsonutils.JSONDict)
for _, key := range image.SortedKeys() {
tmp, _ := image.Get(key)
params.Add(tmp, key)
}
if i == len(images)-1 {
params.Add(jsonutils.NewString(fmt.Sprintf("%s-%s", gi.Name, "root")), "generate_name")
} else {
params.Add(jsonutils.NewString(fmt.Sprintf("%s-%s-%d", gi.Name, "data", i)), "generate_name")
params.Add(jsonutils.JSONTrue, "is_data")
}
model, err := db.DoCreate(ImageManager, ctx, userCred, query, params, ownerId)
if err != nil {
imageIds.Add(jsonutils.NewString(""))
suc = false
break
} else {
func() {
lockman.LockObject(ctx, model)
defer lockman.ReleaseObject(ctx, model)
model.PostCreate(ctx, userCred, ownerId, query, data)
}()
imageIds.Add(jsonutils.NewString(model.GetId()))
}
}
imageNumber, _ := data.Int("image_number")
pendingUsage := SQuota{Image: int(imageNumber)}
QuotaManager.CancelPendingUsage(ctx, userCred, rbacutils.ScopeProject, userCred, nil, &pendingUsage, &pendingUsage)
if !suc {
gi.SetStatus(userCred, api.IMAGE_STATUS_KILLED, "create subimage failed")
}
// HACK
tmp := query.(*jsonutils.JSONDict)
tmp.Add(imageIds, "image_ids")
}
func (gi *SGuestImage) ValidateDeleteCondition(ctx context.Context) error {
if gi.Protected.IsTrue() {
return httperrors.NewForbiddenError("image is protected")
}
return nil
}
func (gi *SGuestImage) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
log.Infof("image delete to nothing")
return nil
}
func (gi *SGuestImage) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
// delete joint
guestJoints, err := GuestImageJointManager.GetByGuestImageId(gi.Id)
if err != nil {
return errors.Wrap(err, "get guest image joint failed")
}
for i := range guestJoints {
guestJoints[i].Delete(ctx, userCred)
}
return gi.SVirtualResourceBase.Delete(ctx, userCred)
}
func (gi *SGuestImage) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject, data jsonutils.JSONObject) error {
images, err := GuestImageJointManager.GetImagesByGuestImageId(gi.Id)
if err != nil {
return errors.Wrap(err, "get images of guest images failed")
}
if len(images) == 0 {
return gi.RealDelete(ctx, userCred)
}
overridePendingDelete := false
purge := false
if query != nil {
overridePendingDelete = jsonutils.QueryBoolean(query, "override_pending_delete", false)
purge = jsonutils.QueryBoolean(query, "purge", false)
}
if gi.Status == api.IMAGE_STATUS_QUEUED {
gi.checkStatus(ctx, userCred)
}
if utils.IsInStringArray(gi.Status, []string{api.IMAGE_STATUS_QUEUED, api.IMAGE_STATUS_KILLED}) {
overridePendingDelete = true
}
return gi.startDeleteTask(ctx, userCred, "", purge, overridePendingDelete)
}
func (gi *SGuestImage) startDeleteTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string,
isPurge bool, overridePendingDelete bool) error {
params := jsonutils.NewDict()
if isPurge {
params.Add(jsonutils.JSONTrue, "purge")
}
if overridePendingDelete {
params.Add(jsonutils.JSONTrue, "override_pending_delete")
}
params.Add(jsonutils.NewString(gi.Status), "image_status")
gi.SetStatus(userCred, api.IMAGE_STATUS_DEACTIVATED, "")
if task, err := taskman.TaskManager.NewTask(ctx, "GuestImageDeleteTask", gi, userCred, params, parentTaskId, "",
nil); err != nil {
return err
} else {
task.ScheduleRun(nil)
}
return nil
}
func (gi *SGuestImage) AllowPerformCancelDelete(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return db.IsAdminAllowPerform(userCred, gi, "cancel-delete")
}
func (gi *SGuestImage) PerformCancelDelete(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
if gi.PendingDeleted {
err := gi.DoCancelPendingDelete(ctx, userCred)
return nil, err
}
return nil, nil
}
func (gi *SGuestImage) DoCancelPendingDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
subImages, err := GuestImageJointManager.GetImagesByGuestImageId(gi.Id)
for i := range subImages {
err = subImages[i].DoCancelPendingDelete(ctx, userCred)
if err != nil {
return errors.Wrapf(err, "subimage %s cancel delete error", subImages[i].GetId())
}
}
err = gi.SVirtualResourceBase.DoCancelPendingDelete(ctx, userCred)
if err != nil {
return err
}
_, err = db.Update(gi, func() error {
gi.Status = api.IMAGE_STATUS_ACTIVE
return nil
})
return errors.Wrap(err, "guest image cancel delete error")
}
type sPair struct {
ID string
Name string
MinDiskMB int32
DiskFormat string
}
func (self *SGuestImage) getMoreDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject,
extra *jsonutils.JSONDict) *jsonutils.JSONDict {
if self.Status != api.IMAGE_STATUS_ACTIVE {
self.checkStatus(ctx, userCred)
extra.Add(jsonutils.NewString(self.Status), "status")
}
images, err := GuestImageJointManager.GetImagesByGuestImageId(self.Id)
if err != nil {
return extra
}
var size int64 = 0
if len(images) == 0 {
extra.Add(jsonutils.NewInt(size), "size")
return extra
}
dataImages := make([]sPair, 0, len(images)-1)
var rootImage sPair
for i := range images {
image := images[i]
size += image.Size
if !image.IsData.IsTrue() {
rootImage = sPair{image.Id, images[i].Name, image.MinDiskMB, image.DiskFormat}
extra.Add(jsonutils.NewInt(int64(image.MinRamMB)), "min_ram_mb")
continue
}
dataImages = append(dataImages, sPair{image.Id, image.Name, image.MinDiskMB, image.DiskFormat})
}
// make sure that the sort of dataimage is fixed
sort.Slice(dataImages, func(i, j int) bool {
return dataImages[i].Name < dataImages[j].Name
})
extra.Add(jsonutils.NewInt(size), "size")
extra.Add(jsonutils.Marshal(rootImage), "root_image")
extra.Add(jsonutils.Marshal(dataImages), "data_images")
// properties of root image
properties, _ := ImagePropertyManager.GetProperties(rootImage.ID)
if err != nil {
return extra
}
propJson := jsonutils.NewDict()
for k, v := range properties {
propJson.Add(jsonutils.NewString(v), k)
}
extra.Add(propJson, "properties")
return extra
}
func (self *SGuestImage) GetCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject) *jsonutils.JSONDict {
extra := self.SSharableVirtualResourceBase.GetCustomizeColumns(ctx, userCred, query)
return self.getMoreDetails(ctx, userCred, query, extra)
}
func (self *SGuestImage) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject) (*jsonutils.JSONDict, error) {
extra, err := self.SSharableVirtualResourceBase.GetExtraDetails(ctx, userCred, query)
if err != nil {
return nil, err
}
if query.Contains("image_ids") {
imageIds, _ := query.Get("image_ids")
extra.Add(imageIds, "image_ids")
return extra, nil
}
return self.getMoreDetails(ctx, userCred, query, extra), nil
}
var checkStatus = map[string]int{
api.IMAGE_STATUS_ACTIVE: 1,
api.IMAGE_STATUS_QUEUED: 2,
api.IMAGE_STATUS_SAVING: 3,
api.IMAGE_STATUS_DEACTIVATED: 4,
api.IMAGE_STATUS_KILLED: 5,
}
func (self *SGuestImage) checkStatus(ctx context.Context, userCred mcclient.TokenCredential) error {
images, err := GuestImageJointManager.GetImagesByGuestImageId(self.Id)
if err != nil {
return err
}
if len(images) == 0 {
return nil
}
status := api.IMAGE_STATUS_ACTIVE
for i := range images {
if checkStatus[images[i].Status] > checkStatus[status] {
status = images[i].Status
}
}
if self.Status != status {
self.SetStatus(userCred, status, "")
self.Status = status
}
return nil
}
func (self *SGuestImage) getSize(ctx context.Context, userCred mcclient.TokenCredential) (int64, error) {
images, err := GuestImageJointManager.GetImagesByGuestImageId(self.Id)
if err != nil {
return 0, err
}
var size int64 = 0
for i := range images {
size += images[i].Size
}
return size, nil
}
func (self *SGuestImageManager) getExpiredPendingDeleteImages() []SGuestImage {
deadline := time.Now().Add(time.Duration(-options.Options.PendingDeleteExpireSeconds) * time.Second)
// there are so many common images of one guest image, so that batch shrink three times
q := self.Query().IsTrue("pending_deleted").LT("pending_deleted_at",
deadline).Limit(options.Options.PendingDeleteMaxCleanBatchSize / 3)
images := make([]SGuestImage, 0)
err := db.FetchModelObjects(self, q, &images)
if err != nil {
log.Errorf("fetch guest images error %s", err)
return nil
}
return images
}
func (self *SGuestImageManager) CleanPendingDeleteImages(ctx context.Context, userCred mcclient.TokenCredential,
isStart bool) {
images := self.getExpiredPendingDeleteImages()
if images == nil {
return
}
for i := range images {
images[i].startDeleteTask(ctx, userCred, "", false, true)
}
}
+122
View File
@@ -0,0 +1,122 @@
// 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 models
import (
"context"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/pkg/errors"
)
type SGuestImageJointManager struct {
db.SJointResourceBaseManager
}
type SGuestImageJoint struct {
db.SJointResourceBase
GuestImageId string `width:"128" charset:"ascii" create:"required"`
ImageId string `width:"128" charset:"ascii" create:"required"`
}
var GuestImageJointManager *SGuestImageJointManager
func init() {
GuestImageJointManager = &SGuestImageJointManager{
db.NewJointResourceBaseManager(
SGuestImageJoint{},
"guest_image_tbl",
"guestimagejoint",
"guestimagejoints",
GuestImageManager,
ImageManager,
),
}
GuestImageJointManager.SetVirtualObject(GuestImageJointManager)
}
func (gm *SGuestImageJointManager) GetByGuestImageId(guestImageId string) ([]SGuestImageJoint, error) {
q := gm.Query().Equals("guest_image_id", guestImageId)
ret := make([]SGuestImageJoint, 0, 1)
err := db.FetchModelObjects(gm, q, &ret)
if err != nil {
return nil, err
}
return ret, nil
}
func (gm *SGuestImageJointManager) GetByImageId(imageId string) (*SGuestImageJoint, error) {
q := gm.Query().Equals("image_id", imageId)
ret := SGuestImageJoint{}
err := q.First(&ret)
if err != nil {
return nil, err
}
return &ret, nil
}
func (gm *SGuestImageJointManager) GetGuestImageByImageId(imageId string) (*SGuestImage, error) {
gits, err := gm.GetByImageId(imageId)
if err != nil {
return nil, err
}
model, err := GuestImageManager.FetchById(gits.GuestImageId)
if err != nil {
return nil, err
}
return model.(*SGuestImage), nil
}
func (gm *SGuestImageJointManager) GetImagesByGuestImageId(guestImageId string) ([]SImage, error) {
giJoints, err := gm.GetByGuestImageId(guestImageId)
if err != nil {
return nil, errors.Wrap(err, "get joints of guest and image failed")
}
if len(giJoints) == 0 {
return []SImage{}, nil
}
imageIds := make([]string, len(giJoints))
for i := range giJoints {
imageIds[i] = giJoints[i].ImageId
}
q := ImageManager.Query().In("id", imageIds)
images := make([]SImage, 0, len(imageIds))
err = db.FetchModelObjects(ImageManager, q, &images)
if err != nil {
return nil, errors.Wrap(err, "fetch images failed")
}
return images, nil
}
func (gt *SGuestImageJoint) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
return db.DeleteModel(ctx, userCred, gt)
}
func (gt *SGuestImageJointManager) CreateGuestImageJoint(ctx context.Context, guestImageId,
imageId string) (*SGuestImageJoint, error) {
gi := SGuestImageJoint{}
gi.GuestImageId = guestImageId
gi.ImageId = imageId
//
if err := gt.TableSpec().Insert(&gi); err != nil {
return nil, errors.Wrapf(err, "insert guestimage joint error")
}
gi.SetVirtualObject(gt)
return &gi, nil
}
+85 -28
View File
@@ -117,8 +117,10 @@ type SImage struct {
MinDiskMB int32 `name:"min_disk" nullable:"false" default:"0" list:"user" create:"optional" update:"user"`
MinRamMB int32 `name:"min_ram" nullable:"false" default:"0" list:"user" create:"optional" update:"user"`
Protected tristate.TriState `nullable:"false" default:"true" list:"user" get:"user" create:"optional" update:"user"`
IsStandard tristate.TriState `nullable:"false" default:"false" list:"user" get:"user" create:"admin_optional"`
Protected tristate.TriState `nullable:"false" default:"true" list:"user" get:"user" create:"optional" update:"user"`
IsStandard tristate.TriState `nullable:"false" default:"false" list:"user" get:"user" create:"admin_optional"`
IsGuestImage tristate.TriState `nullable:"false" default:"false" create:"optional" list:"user"`
IsData tristate.TriState `nullable:"false" default:"false" create:"optional" list:"user"`
// image copy from url, save origin checksum before probe
OssChecksum string `width:"32" charset:"ascii" nullable:"true" get:"user" list:"user"`
@@ -187,20 +189,22 @@ func (self *SImage) CustomizedGetDetailsBody(ctx context.Context, userCred mccli
filePath := self.getLocalLocation()
status := self.Status
formatStr := jsonutils.GetAnyString(query, []string{"format", "disk_format"})
if len(formatStr) > 0 {
subimg := ImageSubformatManager.FetchSubImage(self.Id, formatStr)
if subimg != nil {
isTorrent := jsonutils.QueryBoolean(query, "torrent", false)
if !isTorrent {
filePath = subimg.getLocalLocation()
status = subimg.Status
if self.IsGuestImage.IsFalse() {
formatStr := jsonutils.GetAnyString(query, []string{"format", "disk_format"})
if len(formatStr) > 0 {
subimg := ImageSubformatManager.FetchSubImage(self.Id, formatStr)
if subimg != nil {
isTorrent := jsonutils.QueryBoolean(query, "torrent", false)
if !isTorrent {
filePath = subimg.getLocalLocation()
status = subimg.Status
} else {
filePath = subimg.getLocalTorrentLocation()
status = subimg.TorrentStatus
}
} else {
filePath = subimg.getLocalTorrentLocation()
status = subimg.TorrentStatus
return nil, httperrors.NewNotFoundError("format %s not found", formatStr)
}
} else {
return nil, httperrors.NewNotFoundError("format %s not found", formatStr)
}
}
@@ -356,9 +360,14 @@ func (manager *SImageManager) ValidateCreateData(ctx context.Context, userCred m
return nil, err
}
pendingUsage := SQuota{Image: 1}
if err := QuotaManager.CheckSetPendingQuota(ctx, userCred, rbacutils.ScopeProject, userCred, nil, &pendingUsage); err != nil {
return nil, httperrors.NewOutOfQuotaError("%s", err)
// If this image is the part of guest image (contains "guest_image_id"),
// we do not need to check and set pending quota
// because that pending quota has been checked and set in SGuestImage.ValidateCreateData
if !data.Contains("guest_image_id") {
pendingUsage := SQuota{Image: 1}
if err := QuotaManager.CheckSetPendingQuota(ctx, userCred, rbacutils.ScopeProject, userCred, nil, &pendingUsage); err != nil {
return nil, httperrors.NewOutOfQuotaError("%s", err)
}
}
return data, nil
@@ -371,6 +380,10 @@ func (self *SImage) CustomizeCreate(ctx context.Context, userCred mcclient.Token
}
self.Status = api.IMAGE_STATUS_QUEUED
self.Owner = self.ProjectId
// if belong to a guest image,
if data.Contains("guest_image_id") {
self.IsGuestImage = tristate.True
}
return nil
}
@@ -382,6 +395,11 @@ func (self *SImage) GetPath(format string) string {
return path
}
func (self *SImage) OnJointFailed(ctx context.Context, userCred mcclient.TokenCredential) {
log.Errorf("create joint of image and guest image failed")
self.SetStatus(userCred, api.IMAGE_STATUS_KILLED, "")
}
func (self *SImage) OnSaveFailed(ctx context.Context, userCred mcclient.TokenCredential, msg string) {
self.saveFailed(userCred, msg)
logclient.AddActionLogWithContext(ctx, self, logclient.ACT_IMAGE_SAVE, nil, userCred, false)
@@ -472,8 +490,11 @@ func (self *SImage) SaveImageFromStream(reader io.Reader, calChecksum bool) erro
func (self *SImage) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
self.SVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
pendingUsage := SQuota{Image: 1}
QuotaManager.CancelPendingUsage(ctx, userCred, rbacutils.ScopeProject, userCred, nil, &pendingUsage, &pendingUsage)
// if SImage belong to a guest image, pending quota will not be set.
if self.IsGuestImage.IsFalse() {
pendingUsage := SQuota{Image: 1}
QuotaManager.CancelPendingUsage(ctx, userCred, rbacutils.ScopeProject, userCred, nil, &pendingUsage, &pendingUsage)
}
if data.Contains("properties") {
// update properties
@@ -503,6 +524,16 @@ func (self *SImage) PostCreate(ctx context.Context, userCred mcclient.TokenCrede
self.startImageCopyFromUrlTask(ctx, userCred, copyFrom, "")
}
}
// This image is belong to some guestImage.
// Code below must be run without upload.
if data.Contains("guest_image_id") {
guestImageId, _ := data.GetString("guest_image_id")
_, err := GuestImageJointManager.CreateGuestImageJoint(ctx, guestImageId, self.Id)
if err != nil {
self.OnJointFailed(ctx, userCred)
}
}
}
// After image probe and customization, image size and checksum changed
@@ -523,6 +554,9 @@ func (self *SImage) ImageProbeAndCustomization(
func (self *SImage) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
if self.Status != api.IMAGE_STATUS_QUEUED {
if self.IsGuestImage.IsTrue() {
return nil, httperrors.NewForbiddenError("image is the part of guest imgae")
}
appParams := appsrv.AppContextGetParams(ctx)
if appParams != nil && appParams.Request.ContentLength > 0 {
return nil, httperrors.NewInvalidStatusError("cannot upload in status %s", self.Status)
@@ -530,16 +564,32 @@ func (self *SImage) ValidateUpdateData(ctx context.Context, userCred mcclient.To
} else {
appParams := appsrv.AppContextGetParams(ctx)
if appParams != nil {
isProbe := true
if self.IsData.IsTrue() {
isProbe = false
}
if appParams.Request.ContentLength > 0 {
self.SetStatus(userCred, api.IMAGE_STATUS_SAVING, "update start upload")
err := self.SaveImageFromStream(appParams.Request.Body, false)
// If isProbe is true calculating checksum is not necessary wheng saving from stream,
// otherwise, it is needed.
err := self.SaveImageFromStream(appParams.Request.Body, !isProbe)
if err != nil {
self.OnSaveFailed(ctx, userCred, fmt.Sprintf("update upload failed %s", err))
return nil, httperrors.NewGeneralError(err)
}
self.OnSaveSuccess(ctx, userCred, "update upload success")
data.Remove("status")
self.ImageProbeAndCustomization(ctx, userCred, true)
if !isProbe {
// no probe
self.SetStatus(userCred, api.IMAGE_STATUS_ACTIVE, "data disk image upload success")
} else {
data.Remove("status")
// For guest image, DoConvertAfterProbe is not necessary.
if self.IsGuestImage.IsTrue() {
self.ImageProbeAndCustomization(ctx, userCred, false)
} else {
self.ImageProbeAndCustomization(ctx, userCred, true)
}
}
} else {
copyFrom := appParams.Request.Header.Get(modules.IMAGE_META_COPY_FROM)
if len(copyFrom) > 0 {
@@ -595,6 +645,9 @@ func (self *SImage) ValidateDeleteCondition(ctx context.Context) error {
if self.IsPublic {
return httperrors.NewInvalidStatusError("image is shared")
}
if self.IsGuestImage.IsTrue() {
return httperrors.NewForbiddenError("image is the part of guest image")
}
return self.SVirtualResourceBase.ValidateDeleteCondition(ctx)
}
@@ -687,7 +740,7 @@ func (self *SImage) StartImageConvertTask(ctx context.Context, userCred mcclient
}
func (self *SImage) AllowPerformCancelDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return db.IsAdminAllowPerform(userCred, self, "cancel-delete")
return db.IsAdminAllowPerform(userCred, self, "cancel-delete") && self.IsGuestImage.IsFalse()
}
func (self *SImage) PerformCancelDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
@@ -702,7 +755,9 @@ func (manager *SImageManager) getExpiredPendingDeleteDisks() []SImage {
deadline := time.Now().Add(time.Duration(options.Options.PendingDeleteExpireSeconds*-1) * time.Second)
q := manager.Query()
q = q.IsTrue("pending_deleted").LT("pending_deleted_at", deadline).Limit(options.Options.PendingDeleteMaxCleanBatchSize)
// those images part of guest image will be clean in GuestImageManager.CleanPendingDeleteImages
q = q.IsTrue("pending_deleted").LT("pending_deleted_at",
deadline).Limit(options.Options.PendingDeleteMaxCleanBatchSize).IsFalse("belong_guest_image")
disks := make([]SImage, 0)
err := db.FetchModelObjects(ImageManager, q, &disks)
@@ -720,7 +775,8 @@ func (manager *SImageManager) CleanPendingDeleteImages(ctx context.Context, user
return
}
for i := 0; i < len(disks); i += 1 {
disks[i].startDeleteImageTask(ctx, userCred, "", false, false)
// clean pendingdelete so that overridePendingDelete is true
disks[i].startDeleteImageTask(ctx, userCred, "", false, true)
}
}
@@ -1135,7 +1191,8 @@ func (self *SImage) DoCheckStatus(ctx context.Context, userCred mcclient.TokenCr
}
needConvert := false
subimgs := ImageSubformatManager.GetAllSubImages(self.Id)
if len(subimgs) == 0 {
// for image the part of a guest image, convert is not necessary.
if len(subimgs) == 0 && self.IsGuestImage.IsFalse() {
needConvert = true
}
for i := 0; i < len(subimgs); i += 1 {
@@ -1160,7 +1217,7 @@ func (self *SImage) AllowPerformMarkStandard(
query jsonutils.JSONObject,
data jsonutils.JSONObject,
) bool {
return db.IsAdminAllowPerform(userCred, self, "mark-standard")
return db.IsAdminAllowPerform(userCred, self, "mark-standard") && self.IsGuestImage.IsFalse()
}
func (self *SImage) PerformMarkStandard(
@@ -1188,7 +1245,7 @@ func (self *SImage) PerformMarkStandard(
}
func (self *SImage) AllowPerformUpdateTorrentStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return true
return self.IsGuestImage.IsFalse()
}
func (self *SImage) PerformUpdateTorrentStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
+4
View File
@@ -54,6 +54,8 @@ func initHandlers(app *appsrv.Application) {
models.ImagePropertyManager,
models.ImageSubformatManager,
models.GuestImageJointManager,
models.QuotaManager,
models.QuotaUsageManager,
} {
@@ -64,6 +66,8 @@ func initHandlers(app *appsrv.Application) {
db.OpsLog,
db.Metadata,
models.ImageManager,
models.GuestImageManager,
} {
db.RegisterModelManager(manager)
handler := db.NewModelHandler(manager)
+2
View File
@@ -103,6 +103,8 @@ func StartService() {
cron := cronman.InitCronJobManager(true, options.Options.CronJobWorkerCount)
cron.AddJobAtIntervals("CleanPendingDeleteImages", time.Duration(options.Options.PendingDeleteCheckSeconds)*time.Second, models.ImageManager.CleanPendingDeleteImages)
cron.AddJobAtIntervals("CalculateQuotaUsages", time.Duration(opts.CalculateQuotaUsageIntervalSeconds)*time.Second, models.QuotaManager.CalculateQuotaUsages)
cron.AddJobAtIntervals("CleanPendingDeleteGuestImages",
time.Duration(options.Options.PendingDeleteCheckSeconds)*time.Second, models.GuestImageManager.CleanPendingDeleteImages)
cron.Start()
}
+113
View File
@@ -0,0 +1,113 @@
// 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/log"
api "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/image/models"
"yunion.io/x/onecloud/pkg/image/options"
"yunion.io/x/onecloud/pkg/util/logclient"
)
type GuestImageDeleteTask struct {
taskman.STask
}
func init() {
taskman.RegisterTask(GuestImageDeleteTask{})
}
func (self *GuestImageDeleteTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
guestImage := obj.(*models.SGuestImage)
isPurge := jsonutils.QueryBoolean(self.Params, "purge", false)
isOverridePendingDelete := jsonutils.QueryBoolean(self.Params, "override_pending_delete", false)
if options.Options.EnablePendingDelete && !isPurge && !isOverridePendingDelete {
if guestImage.PendingDeleted {
self.SetStageComplete(ctx, nil)
}
self.startPendingDelete(ctx, guestImage)
} else {
self.startDelete(ctx, guestImage)
}
}
func (self *GuestImageDeleteTask) startPendingDelete(ctx context.Context, guestImage *models.SGuestImage) {
images, err := models.GuestImageJointManager.GetImagesByGuestImageId(guestImage.GetId())
if err != nil {
self.taskFailed(ctx, guestImage, err.Error())
}
for i := range images {
images[i].StopTorrents()
err := images[i].DoPendingDelete(ctx, self.UserCred)
if err != nil {
self.taskFailed(ctx, guestImage, fmt.Sprintf("image %s pending delete failed", images[i].GetId()))
return
}
}
err = guestImage.DoPendingDelete(ctx, self.UserCred)
if err != nil {
self.taskFailed(ctx, guestImage, fmt.Sprintf("guest image %s pending delete failed", guestImage.GetId()))
}
self.SetStageComplete(ctx, nil)
}
func (self *GuestImageDeleteTask) startDelete(ctx context.Context, guestImage *models.SGuestImage) {
images, err := models.GuestImageJointManager.GetImagesByGuestImageId(guestImage.GetId())
if err != nil {
self.taskFailed(ctx, guestImage, err.Error())
}
for i := range images {
err := images[i].RemoveFiles()
if err != nil {
self.taskFailed(ctx, guestImage, fmt.Sprintf("fail to remove %s: %s", images[i].GetPath(""), err))
return
}
err = images[i].SetStatus(self.UserCred, api.IMAGE_STATUS_DELETED, "delete")
if err != nil {
self.taskFailed(ctx, guestImage, fmt.Sprintf("fail to set image %s status ", images[i].GetId()))
return
}
err = images[i].RealDelete(ctx, self.UserCred)
if err != nil {
self.taskFailed(ctx, guestImage, fmt.Sprintf("fail to real delete image %s", images[i].GetId()))
return
}
}
err = guestImage.SetStatus(self.UserCred, api.IMAGE_STATUS_DELETED, "delete")
if err != nil {
self.taskFailed(ctx, guestImage, fmt.Sprintf("fail to set guest image status %s", guestImage.GetId()))
}
err = guestImage.RealDelete(ctx, self.UserCred)
if err != nil {
self.taskFailed(ctx, guestImage, fmt.Sprintf("fail to real delete guest image %s", guestImage.GetId()))
}
self.SetStageComplete(ctx, nil)
}
func (self *GuestImageDeleteTask) taskFailed(ctx context.Context, guestImage *models.SGuestImage, reason string) {
log.Errorf("Guest Image %s delete failed: %s", guestImage.Id, reason)
db.OpsLog.LogEvent(guestImage, db.ACT_IMAGE_DELETE_FAIL, reason, self.UserCred)
logclient.AddActionLogWithContext(ctx, guestImage, logclient.ACT_DELETE, reason, self.UserCred, false)
self.SetStageFailed(ctx, reason)
}
+5 -1
View File
@@ -69,7 +69,11 @@ func (self *ImageCopyFromUrlTask) OnInit(ctx context.Context, obj db.IStandalone
func (self *ImageCopyFromUrlTask) OnImageImportComplete(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
image := obj.(*models.SImage)
image.OnSaveTaskSuccess(self, self.UserCred, "create upload success")
image.ImageProbeAndCustomization(ctx, self.UserCred, true)
if image.IsGuestImage.IsTrue() {
image.ImageProbeAndCustomization(ctx, self.UserCred, false)
} else {
image.ImageProbeAndCustomization(ctx, self.UserCred, true)
}
self.SetStageComplete(ctx, nil)
}
+2
View File
@@ -165,6 +165,7 @@ func (self *ImageProbeTask) OnProbeFailed(ctx context.Context, image *models.SIm
self.SetStage("OnConvertComplete", nil)
image.StartImageConvertTask(ctx, self.UserCred, self.GetId())
} else {
image.SetStatus(self.UserCred, api.IMAGE_STATUS_ACTIVE, "")
self.SetStageFailed(ctx, reason)
}
}
@@ -179,6 +180,7 @@ func (self *ImageProbeTask) OnProbeSuccess(ctx context.Context, image *models.SI
self.SetStage("OnConvertComplete", nil)
image.StartImageConvertTask(ctx, self.UserCred, self.GetId())
} else {
image.SetStatus(self.UserCred, api.IMAGE_STATUS_ACTIVE, "")
self.SetStageComplete(ctx, nil)
}
}