mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-21 06:09:39 +08:00
feature: guest image create from vm, delete, update, list; create vm
from guest image.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user