Files
cloudpods/pkg/image/models/images.go
2020-04-11 06:50:48 +08:00

1467 lines
47 KiB
Go

// 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"
"io"
"math"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/tristate"
"yunion.io/x/pkg/util/timeutils"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/apis"
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/quotas"
"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/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/util/fileutils2"
"yunion.io/x/onecloud/pkg/util/logclient"
"yunion.io/x/onecloud/pkg/util/qemuimg"
"yunion.io/x/onecloud/pkg/util/rbacutils"
"yunion.io/x/onecloud/pkg/util/streamutils"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
const (
LocalFilePrefix = "file://"
)
type SImageManager struct {
db.SSharableVirtualResourceBaseManager
}
var ImageManager *SImageManager
var imgStreamingWorkerMan *appsrv.SWorkerManager
func init() {
ImageManager = &SImageManager{
SSharableVirtualResourceBaseManager: db.NewSharableVirtualResourceBaseManager(
SImage{},
"images",
"image",
"images",
),
}
ImageManager.SetVirtualObject(ImageManager)
imgStreamingWorkerMan = appsrv.NewWorkerManager("image_streaming_worker", 10, 1024, true)
}
/*
+------------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------------+--------------+------+-----+---------+-------+
| id | varchar(36) | NO | PRI | NULL | |
| name | varchar(255) | YES | | NULL | |
| size | bigint(20) | YES | | NULL | |
| status | varchar(30) | NO | | NULL | |
| is_public | tinyint(1) | NO | MUL | NULL | |
| location | text | YES | | NULL | |
| created_at | datetime | NO | | NULL | |
| updated_at | datetime | YES | | NULL | |
| deleted_at | datetime | YES | | NULL | |
| deleted | tinyint(1) | NO | MUL | NULL | |
| parent_id | varchar(36) | YES | | NULL | |
| disk_format | varchar(20) | YES | | NULL | |
| container_format | varchar(20) | YES | | NULL | |
| checksum | varchar(32) | YES | | NULL | |
| owner | varchar(255) | YES | | NULL | |
| min_disk | int(11) | NO | | NULL | |
| min_ram | int(11) | NO | | NULL | |
| protected | tinyint(1) | YES | | NULL | |
| description | varchar(256) | YES | | NULL | |
+------------------+--------------+------+-----+---------+-------+
*/
type SImage struct {
db.SSharableVirtualResourceBase
// 镜像大小, 单位Byte
Size int64 `nullable:"true" list:"user" create:"optional"`
// 存储地址
Location string `nullable:"true"`
// 镜像格式
DiskFormat string `width:"20" charset:"ascii" nullable:"true" list:"user" create:"optional" default:"raw"`
// 校验和
Checksum string `width:"32" charset:"ascii" nullable:"true" get:"user" list:"user"`
FastHash string `width:"32" charset:"ascii" nullable:"true" get:"user"`
// 用户Id
Owner string `width:"255" charset:"ascii" nullable:"true" get:"user"`
// 最小系统盘要求
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"`
// 是否是主机镜像
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"`
}
func (manager *SImageManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) {
manager.SVirtualResourceBaseManager.CustomizeHandlerInfo(info)
switch info.GetName(nil) {
case "get_details", "create", "update":
info.SetProcessTimeout(time.Minute * 120).SetWorkerManager(imgStreamingWorkerMan)
}
}
func (manager *SImageManager) FetchCreateHeaderData(ctx context.Context, header http.Header) (jsonutils.JSONObject, error) {
return modules.FetchImageMeta(header), nil
}
func (manager *SImageManager) FetchUpdateHeaderData(ctx context.Context, header http.Header) (jsonutils.JSONObject, error) {
return modules.FetchImageMeta(header), nil
}
func (manager *SImageManager) InitializeData() error {
// set cloudregion ID
images := make([]SImage, 0)
q := manager.Query().IsNullOrEmpty("tenant_id")
err := db.FetchModelObjects(manager, q, &images)
if err != nil {
return err
}
for i := 0; i < len(images); i += 1 {
if len(images[i].ProjectId) == 0 {
db.Update(&images[i], func() error {
images[i].ProjectId = images[i].Owner
return nil
})
}
}
return nil
}
func (manager *SImageManager) AllowGetPropertyDetail(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return true
}
func (manager *SImageManager) GetPropertyDetail(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
appParams := appsrv.AppContextGetParams(ctx)
appParams.OverrideResponseBodyWrapper = true
queryDict := query.(*jsonutils.JSONDict)
queryDict.Add(jsonutils.JSONTrue, "details")
items, err := db.ListItems(manager, ctx, userCred, queryDict, nil)
if err != nil {
log.Errorf("Fail to list items: %s", err)
return nil, httperrors.NewGeneralError(err)
}
return modulebase.ListResult2JSONWithKey(items, manager.KeywordPlural()), nil
}
func (manager *SImageManager) IsCustomizedGetDetailsBody() bool {
return true
}
func (self *SImage) CustomizedGetDetailsBody(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
filePath := self.getLocalLocation()
status := self.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 {
return nil, httperrors.NewNotFoundError("format %s not found", formatStr)
}
}
}
if status != api.IMAGE_STATUS_ACTIVE {
return nil, httperrors.NewInvalidStatusError("cannot download in status %s", status)
}
if filePath == "" {
return nil, httperrors.NewInvalidStatusError("empty file path")
}
appParams := appsrv.AppContextGetParams(ctx)
fstat, err := os.Stat(filePath)
if err != nil {
return nil, errors.Wrap(err, "os.Stat")
}
appParams.Response.Header().Set("Content-Length", strconv.FormatInt(fstat.Size(), 10))
fp, err := os.Open(filePath)
if err != nil {
return nil, errors.Wrap(err, "os.Open")
}
defer fp.Close()
_, err = streamutils.StreamPipe(fp, appParams.Response, false, nil)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
return nil, nil
}
func (self *SImage) getMoreDetails(out api.ImageDetails) api.ImageDetails {
properties, err := ImagePropertyManager.GetProperties(self.Id)
if err != nil {
log.Errorf("ImagePropertyManager.GetProperties fail %s", err)
}
out.Properties = properties
if self.PendingDeleted {
pendingDeletedAt := self.PendingDeletedAt.Add(time.Second * time.Duration(options.Options.PendingDeleteExpireSeconds))
out.AutoDeleteAt = pendingDeletedAt
}
var ossChksum = self.OssChecksum
if len(self.OssChecksum) == 0 {
ossChksum = self.Checksum
}
out.OssChecksum = ossChksum
out.DisableDelete = self.Protected.Bool()
return out
}
func (manager *SImageManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
objs []interface{},
fields stringutils2.SSortedStrings,
isList bool,
) []api.ImageDetails {
rows := make([]api.ImageDetails, len(objs))
virtRows := manager.SSharableVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
for i := range rows {
image := objs[i].(*SImage)
rows[i] = api.ImageDetails{
SharableVirtualResourceDetails: virtRows[i],
}
rows[i] = image.getMoreDetails(rows[i])
}
return rows
}
func (self *SImage) GetExtraDetails(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
isList bool,
) (api.ImageDetails, error) {
return api.ImageDetails{}, nil
}
func (self *SImage) GetExtraDetailsHeaders(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) map[string]string {
headers := make(map[string]string)
details := ImageManager.FetchCustomizeColumns(ctx, userCred, query, []interface{}{self}, nil, false)
extra := jsonutils.Marshal(details[0]).(*jsonutils.JSONDict)
for _, k := range extra.SortedKeys() {
if k == "properties" {
continue
}
val, _ := extra.GetString(k)
if len(val) > 0 {
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, k)] = val
}
}
jsonDict := jsonutils.Marshal(self).(*jsonutils.JSONDict)
fields, _ := db.GetDetailFields(self.GetModelManager(), userCred)
for _, k := range jsonDict.SortedKeys() {
if utils.IsInStringArray(k, fields) {
val, _ := jsonDict.GetString(k)
if len(val) > 0 {
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, k)] = val
}
}
}
formatStr := jsonutils.GetAnyString(query, []string{"format", "disk_format"})
if len(formatStr) > 0 {
subimg := ImageSubformatManager.FetchSubImage(self.Id, formatStr)
if subimg != nil {
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, "disk_format")] = formatStr
isTorrent := jsonutils.QueryBoolean(query, "torrent", false)
if !isTorrent {
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, "status")] = subimg.Status
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, "size")] = fmt.Sprintf("%d", subimg.Size)
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, "checksum")] = subimg.Checksum
} else {
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, "status")] = subimg.TorrentStatus
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, "size")] = fmt.Sprintf("%d", subimg.TorrentSize)
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, "checksum")] = subimg.TorrentChecksum
}
}
}
// none of subimage business
var ossChksum = self.OssChecksum
if len(self.OssChecksum) == 0 {
ossChksum = self.Checksum
}
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, "oss_checksum")] = ossChksum
properties, _ := ImagePropertyManager.GetProperties(self.Id)
if len(properties) > 0 {
for k, v := range properties {
headers[fmt.Sprintf("%s%s", modules.IMAGE_META_PROPERTY, k)] = v
}
}
if self.PendingDeleted {
pendingDeletedAt := self.PendingDeletedAt.Add(time.Second * time.Duration(options.Options.PendingDeleteExpireSeconds))
headers[fmt.Sprintf("%s%s", modules.IMAGE_META, "auto_delete_at")] = timeutils.FullIsoTime(pendingDeletedAt)
}
return headers
}
func (manager *SImageManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
input := apis.VirtualResourceCreateInput{}
err := data.Unmarshal(&input)
if err != nil {
return nil, httperrors.NewInternalServerError("unmarshal StandaloneResourceCreateInput fail %s", err)
}
input, err = manager.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input)
if err != nil {
return nil, err
}
data.Update(jsonutils.Marshal(input))
// 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}
keys := imageCreateInput2QuotaKeys(data, ownerId)
pendingUsage.SetKeys(keys)
if err := quotas.CheckSetPendingQuota(ctx, userCred, &pendingUsage); err != nil {
return nil, httperrors.NewOutOfQuotaError("%s", err)
}
}
return data, nil
}
func (self *SImage) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
err := self.SVirtualResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
if err != nil {
return err
}
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
}
func (self *SImage) GetPath(format string) string {
path := filepath.Join(options.Options.FilesystemStoreDatadir, self.Id)
if len(format) > 0 {
path = fmt.Sprintf("%s.%s", path, format)
}
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, msg, userCred, false)
}
func (self *SImage) OnSaveTaskFailed(task taskman.ITask, userCred mcclient.TokenCredential, msg string) {
self.saveFailed(userCred, msg)
logclient.AddActionLogWithStartable(task, self, logclient.ACT_IMAGE_SAVE, msg, userCred, false)
}
func (self *SImage) OnSaveSuccess(ctx context.Context, userCred mcclient.TokenCredential, msg string) {
self.saveSuccess(userCred, msg)
logclient.AddActionLogWithContext(ctx, self, logclient.ACT_IMAGE_SAVE, msg, userCred, true)
}
func (self *SImage) OnSaveTaskSuccess(task taskman.ITask, userCred mcclient.TokenCredential, msg string) {
self.saveSuccess(userCred, msg)
logclient.AddActionLogWithStartable(task, self, logclient.ACT_IMAGE_SAVE, msg, userCred, true)
}
func (self *SImage) saveSuccess(userCred mcclient.TokenCredential, msg string) {
// do not set this status, until image converting complete
// self.SetStatus(userCred, api.IMAGE_STATUS_ACTIVE, msg)
db.OpsLog.LogEvent(self, db.ACT_SAVE, msg, userCred)
}
func (self *SImage) saveFailed(userCred mcclient.TokenCredential, msg string) {
log.Errorf(msg)
self.SetStatus(userCred, api.IMAGE_STATUS_KILLED, msg)
db.OpsLog.LogEvent(self, db.ACT_SAVE_FAIL, msg, userCred)
}
func (self *SImage) saveImageFromStream(localPath string, reader io.Reader, calChecksum bool) (*streamutils.SStreamProperty, error) {
fp, err := os.Create(localPath)
if err != nil {
return nil, err
}
defer fp.Close()
lastSaveTime := time.Now()
return streamutils.StreamPipe(reader, fp, calChecksum, func(saved int64) {
now := time.Now()
if now.Sub(lastSaveTime) > 5*time.Second {
self.saveSize(saved)
lastSaveTime = now
}
})
}
func (self *SImage) saveSize(newSize int64) error {
_, err := db.Update(self, func() error {
self.Size = newSize
return nil
})
if err != nil {
return errors.Wrap(err, "Update size")
}
return nil
}
//Image always do probe and customize after save from stream
func (self *SImage) SaveImageFromStream(reader io.Reader, calChecksum bool) error {
localPath := self.GetPath("")
sp, err := self.saveImageFromStream(localPath, reader, calChecksum)
if err != nil {
log.Errorf("saveImageFromStream fail %s", err)
return err
}
virtualSizeBytes := int64(0)
format := ""
img, err := qemuimg.NewQemuImage(localPath)
if err != nil {
return err
}
format = string(img.Format)
virtualSizeBytes = img.SizeBytes
var fastChksum string
if calChecksum {
fastChksum, err = fileutils2.FastCheckSum(localPath)
if err != nil {
return err
}
}
db.Update(self, func() error {
self.Size = sp.Size
if calChecksum {
self.Checksum = sp.CheckSum
self.FastHash = fastChksum
}
self.Location = fmt.Sprintf("%s%s", LocalFilePrefix, localPath)
if len(format) > 0 {
self.DiskFormat = format
}
if virtualSizeBytes > 0 {
self.MinDiskMB = int32(math.Ceil(float64(virtualSizeBytes) / 1024 / 1024))
}
return nil
})
return nil
}
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)
// if SImage belong to a guest image, pending quota will not be set.
if self.IsGuestImage.IsFalse() {
pendingUsage := SQuota{Image: 1}
keys := imageCreateInput2QuotaKeys(data, ownerId)
pendingUsage.SetKeys(keys)
cancelUsage := SQuota{Image: 1}
keys = self.GetQuotaKeys()
cancelUsage.SetKeys(keys)
quotas.CancelPendingUsage(ctx, userCred, &pendingUsage, &cancelUsage, true)
}
if data.Contains("properties") {
// update properties
props, _ := data.Get("properties")
err := ImagePropertyManager.SaveProperties(ctx, userCred, self.Id, props)
if err != nil {
log.Warningf("save properties error %s", err)
}
}
appParams := appsrv.AppContextGetParams(ctx)
if appParams.Request.ContentLength > 0 {
db.OpsLog.LogEvent(self, db.ACT_SAVING, "create upload", userCred)
self.SetStatus(userCred, api.IMAGE_STATUS_SAVING, "create upload")
err := self.SaveImageFromStream(appParams.Request.Body, false)
if err != nil {
self.OnSaveFailed(ctx, userCred, fmt.Sprintf("create upload fail %s", err))
return
}
self.OnSaveSuccess(ctx, userCred, "create upload success")
self.ImageProbeAndCustomization(ctx, userCred, true)
} else {
copyFrom := appParams.Request.Header.Get(modules.IMAGE_META_COPY_FROM)
if len(copyFrom) > 0 {
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
// will recalculate checksum in the end
func (self *SImage) ImageProbeAndCustomization(
ctx context.Context, userCred mcclient.TokenCredential, doConvertAfterProbe bool,
) error {
data := jsonutils.NewDict()
data.Set("do_convert", jsonutils.NewBool(doConvertAfterProbe))
task, err := taskman.TaskManager.NewTask(
ctx, "ImageProbeTask", self, userCred, data, "", "", nil)
if err != nil {
return err
}
task.ScheduleRun(nil)
return nil
}
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.CanUpdate(data) {
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)
}
} 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")
// 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")
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 {
err := self.startImageCopyFromUrlTask(ctx, userCred, copyFrom, "")
if err != nil {
self.OnSaveFailed(ctx, userCred, fmt.Sprintf("update copy from url failed %s", err))
return nil, httperrors.NewGeneralError(err)
}
}
}
}
}
input := apis.VirtualResourceBaseUpdateInput{}
err := data.Unmarshal(&input)
if err != nil {
return nil, errors.Wrap(err, "Unmarshal")
}
input, err = self.SVirtualResourceBase.ValidateUpdateData(ctx, userCred, query, input)
if err != nil {
return nil, errors.Wrap(err, "SVirtualResourceBase.ValidateUpdateData")
}
data.Update(jsonutils.Marshal(input))
return data, nil
}
func (self *SImage) PreUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) {
self.SVirtualResourceBase.PreUpdate(ctx, userCred, query, data)
}
func (self *SImage) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) {
self.SVirtualResourceBase.PostUpdate(ctx, userCred, query, data)
if data.Contains("properties") {
// update properties
props, _ := data.Get("properties")
err := ImagePropertyManager.SaveProperties(ctx, userCred, self.Id, props)
if err != nil {
log.Errorf("save properties error %s", err)
}
}
}
func (self *SImage) AllowDeleteItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
overridePendingDelete := false
purge := false
if query != nil {
overridePendingDelete = jsonutils.QueryBoolean(query, "override_pending_delete", false)
purge = jsonutils.QueryBoolean(query, "purge", false)
}
if (overridePendingDelete || purge) && !db.IsAdminAllowDelete(userCred, self) {
return false
}
return self.IsOwner(userCred) || db.IsAdminAllowDelete(userCred, self)
}
func (self *SImage) ValidateDeleteCondition(ctx context.Context) error {
if self.Protected.IsTrue() {
return httperrors.NewForbiddenError("image is protected")
}
if self.IsGuestImage.IsTrue() {
return httperrors.NewForbiddenError("image is the part of guest image")
}
if self.IsShared() {
return httperrors.NewForbiddenError("image is shared")
}
return self.SVirtualResourceBase.ValidateDeleteCondition(ctx)
}
func (self *SImage) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
log.Infof("image delete do nothing")
return nil
}
func (self *SImage) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
return self.SVirtualResourceBase.Delete(ctx, userCred)
}
func (self *SImage) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
overridePendingDelete := false
purge := false
if query != nil {
overridePendingDelete = jsonutils.QueryBoolean(query, "override_pending_delete", false)
purge = jsonutils.QueryBoolean(query, "purge", false)
}
if utils.IsInStringArray(self.Status, []string{
api.IMAGE_STATUS_KILLED,
api.IMAGE_STATUS_QUEUED,
}) {
overridePendingDelete = true
}
return self.startDeleteImageTask(ctx, userCred, "", purge, overridePendingDelete)
}
func (self *SImage) startDeleteImageTask(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(self.Status), "image_status")
self.SetStatus(userCred, api.IMAGE_STATUS_DEACTIVATED, "")
task, err := taskman.TaskManager.NewTask(ctx, "ImageDeleteTask", self, userCred, params, parentTaskId, "", nil)
if err != nil {
return err
}
task.ScheduleRun(nil)
return nil
}
func (self *SImage) startImageCopyFromUrlTask(ctx context.Context, userCred mcclient.TokenCredential, copyFrom string, parentTaskId string) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(copyFrom), "copy_from")
msg := fmt.Sprintf("copy from url %s", copyFrom)
self.SetStatus(userCred, api.IMAGE_STATUS_SAVING, msg)
db.OpsLog.LogEvent(self, db.ACT_SAVING, msg, userCred)
task, err := taskman.TaskManager.NewTask(ctx, "ImageCopyFromUrlTask", self, userCred, params, parentTaskId, "", nil)
if err != nil {
return err
}
task.ScheduleRun(nil)
return nil
}
func (self *SImage) StartImageCheckTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error {
task, err := taskman.TaskManager.NewTask(ctx, "ImageCheckTask", self, userCred, nil, parentTaskId, "", nil)
if err != nil {
return err
}
task.ScheduleRun(nil)
return nil
}
func (self *SImage) StartImageConvertTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error {
err := self.MigrateSubImage()
if err != nil {
return err
}
err = self.MakeSubImages()
if err != nil {
return err
}
task, err := taskman.TaskManager.NewTask(ctx, "ImageConvertTask", self, userCred, nil, parentTaskId, "", nil)
if err != nil {
return err
}
task.ScheduleRun(nil)
return nil
}
func (self *SImage) AllowPerformCancelDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
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) {
if self.PendingDeleted {
err := self.DoCancelPendingDelete(ctx, userCred)
return nil, err
}
return nil, nil
}
func (manager *SImageManager) getExpiredPendingDeleteDisks() []SImage {
deadline := time.Now().Add(time.Duration(options.Options.PendingDeleteExpireSeconds*-1) * time.Second)
q := manager.Query()
// 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("is_guest_image")
disks := make([]SImage, 0)
err := db.FetchModelObjects(ImageManager, q, &disks)
if err != nil {
log.Errorf("fetch disks error %s", err)
return nil
}
return disks
}
func (manager *SImageManager) CleanPendingDeleteImages(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) {
disks := manager.getExpiredPendingDeleteDisks()
if disks == nil {
return
}
for i := 0; i < len(disks); i += 1 {
// clean pendingdelete so that overridePendingDelete is true
disks[i].startDeleteImageTask(ctx, userCred, "", false, true)
}
}
func (self *SImage) DoPendingDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
err := self.SVirtualResourceBase.DoPendingDelete(ctx, userCred)
if err != nil {
return err
}
_, err = db.Update(self, func() error {
self.Status = api.IMAGE_STATUS_PENDING_DELETE
return nil
})
return err
}
func (self *SImage) DoCancelPendingDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
err := self.SVirtualResourceBase.DoCancelPendingDelete(ctx, userCred)
if err != nil {
return err
}
_, err = db.Update(self, func() error {
self.Status = api.IMAGE_STATUS_ACTIVE
return nil
})
return err
}
type SImageUsage struct {
Count int64
Size int64
}
func (manager *SImageManager) count(scope rbacutils.TRbacScope, ownerId mcclient.IIdentityProvider, status string, isISO tristate.TriState, pendingDelete bool, guestImage tristate.TriState) map[string]SImageUsage {
sq := manager.Query("id")
switch scope {
case rbacutils.ScopeSystem:
// do nothing
case rbacutils.ScopeDomain:
sq = sq.Equals("domain_id", ownerId.GetProjectDomainId())
case rbacutils.ScopeProject:
sq = sq.Equals("tenant_id", ownerId.GetProjectId())
}
// exclude GuestImage!!!
if guestImage.IsTrue() {
sq = sq.IsTrue("is_guest_image")
} else if guestImage.IsFalse() {
sq = sq.IsFalse("is_guest_image")
}
if len(status) > 0 {
sq = sq.Equals("status", status)
}
sq = sq.NotEquals("status", api.IMAGE_STATUS_KILLED)
if pendingDelete {
sq = sq.IsTrue("pending_deleted")
} else {
sq = sq.IsFalse("pending_deleted")
}
if isISO.IsTrue() {
sq = sq.Equals("disk_format", "iso")
} else if isISO.IsFalse() {
sq = sq.NotEquals("disk_format", "iso")
}
cnt, _ := sq.CountWithError()
subimages := ImageSubformatManager.Query().SubQuery()
q := subimages.Query(subimages.Field("format"),
sqlchemy.COUNT("count"),
sqlchemy.SUM("size", subimages.Field("size")))
q = q.In("image_id", sq.SubQuery())
q = q.GroupBy(subimages.Field("format"))
type sFormatImageUsage struct {
Format string
Count int64
Size int64
}
var usages []sFormatImageUsage
err := q.All(&usages)
if err != nil {
log.Errorf("query usage fail %s", err)
return nil
}
ret := make(map[string]SImageUsage)
totalSize := int64(0)
for _, u := range usages {
ret[u.Format] = SImageUsage{Count: u.Count, Size: u.Size}
totalSize += u.Size
}
ret["total"] = SImageUsage{Count: int64(cnt), Size: totalSize}
return ret
}
func expandUsageCount(usages map[string]int64, prefix, imgType, state string, count map[string]SImageUsage) {
for k, u := range count {
key := []string{}
if len(prefix) > 0 {
key = append(key, prefix)
}
key = append(key, imgType)
if len(state) > 0 {
key = append(key, state)
}
key = append(key, k)
countKey := strings.Join(append(key, "count"), ".")
sizeKey := strings.Join(append(key, "size"), ".")
usages[countKey] = u.Count
usages[sizeKey] = u.Size
}
}
func (manager *SImageManager) Usage(scope rbacutils.TRbacScope, ownerId mcclient.IIdentityProvider, prefix string) map[string]int64 {
usages := make(map[string]int64)
count := manager.count(scope, ownerId, api.IMAGE_STATUS_ACTIVE, tristate.False, false, tristate.False)
expandUsageCount(usages, prefix, "img", "", count)
count = manager.count(scope, ownerId, api.IMAGE_STATUS_ACTIVE, tristate.True, false, tristate.False)
expandUsageCount(usages, prefix, "iso", "", count)
count = manager.count(scope, ownerId, api.IMAGE_STATUS_ACTIVE, tristate.None, false, tristate.False)
expandUsageCount(usages, prefix, "imgiso", "", count)
count = manager.count(scope, ownerId, "", tristate.False, true, tristate.False)
expandUsageCount(usages, prefix, "img", "pending_delete", count)
count = manager.count(scope, ownerId, "", tristate.True, true, tristate.False)
expandUsageCount(usages, prefix, "iso", "pending_delete", count)
count = manager.count(scope, ownerId, "", tristate.None, true, tristate.False)
expandUsageCount(usages, prefix, "imgiso", "pending_delete", count)
return usages
}
func (self *SImage) GetImageType() api.TImageType {
if self.DiskFormat == string(qemuimg.ISO) {
return api.ImageTypeISO
} else {
return api.ImageTypeTemplate
}
}
func (self *SImage) newSubformat(format qemuimg.TImageFormat, migrate bool) error {
subformat := &SImageSubformat{}
subformat.SetModelManager(ImageSubformatManager, subformat)
subformat.ImageId = self.Id
subformat.Format = string(format)
if migrate {
subformat.Size = self.Size
subformat.Checksum = self.Checksum
subformat.FastHash = self.FastHash
subformat.Status = api.IMAGE_STATUS_ACTIVE
subformat.Location = self.Location
} else {
subformat.Status = api.IMAGE_STATUS_QUEUED
}
subformat.TorrentStatus = api.IMAGE_STATUS_QUEUED
err := ImageSubformatManager.TableSpec().Insert(subformat)
if err != nil {
log.Errorf("fail to make subformat %s: %s", format, err)
return err
}
return nil
}
func (self *SImage) MigrateSubImage() error {
if !qemuimg.IsSupportedImageFormat(self.DiskFormat) {
log.Warningf("Unsupported image format %s, no need to migrate", self.DiskFormat)
return nil
}
subimg := ImageSubformatManager.FetchSubImage(self.Id, self.DiskFormat)
if subimg != nil {
return nil
}
imgInst, err := self.getQemuImage()
if err != nil {
return err
}
if self.GetImageType() != api.ImageTypeISO && imgInst.IsSparse() {
// need to convert again
return self.newSubformat(qemuimg.String2ImageFormat(self.DiskFormat), false)
} else {
localPath := self.getLocalLocation()
if !strings.HasSuffix(localPath, fmt.Sprintf(".%s", self.DiskFormat)) {
newLocalpath := fmt.Sprintf("%s.%s", localPath, self.DiskFormat)
cmd := exec.Command("mv", "-f", localPath, newLocalpath)
err := cmd.Run()
if err != nil {
return err
}
_, err = db.Update(self, func() error {
self.Location = fmt.Sprintf("%s%s", LocalFilePrefix, newLocalpath)
return nil
})
if err != nil {
return err
}
}
return self.newSubformat(qemuimg.String2ImageFormat(self.DiskFormat), true)
}
}
func (self *SImage) MakeSubImages() error {
if self.GetImageType() == api.ImageTypeISO {
return nil
}
log.Debugf("[MakeSubImages] convert image to %#v", options.Options.TargetImageFormats)
for _, format := range options.Options.TargetImageFormats {
if !qemuimg.IsSupportedImageFormat(format) {
continue
}
if format != self.DiskFormat {
// need to create a record
subformat := ImageSubformatManager.FetchSubImage(self.Id, format)
if subformat == nil {
err := self.newSubformat(qemuimg.String2ImageFormat(format), false)
if err != nil {
return err
}
}
}
}
return nil
}
func (self *SImage) ConvertAllSubformats() error {
subimgs := ImageSubformatManager.GetAllSubImages(self.Id)
for i := 0; i < len(subimgs); i += 1 {
if !utils.IsInStringArray(subimgs[i].Format, options.Options.TargetImageFormats) {
continue
}
err := subimgs[i].DoConvert(self)
if err != nil {
return err
}
}
return nil
}
func (self *SImage) getLocalLocation() string {
if len(self.Location) > len(LocalFilePrefix) {
return self.Location[len(LocalFilePrefix):]
}
return ""
}
func (self *SImage) getQemuImage() (*qemuimg.SQemuImage, error) {
return qemuimg.NewQemuImageWithIOLevel(self.getLocalLocation(), qemuimg.IONiceIdle)
}
func (self *SImage) StopTorrents() {
subimgs := ImageSubformatManager.GetAllSubImages(self.Id)
for i := 0; i < len(subimgs); i += 1 {
subimgs[i].StopTorrent()
}
}
func (self *SImage) seedTorrents() {
subimgs := ImageSubformatManager.GetAllSubImages(self.Id)
for i := 0; i < len(subimgs); i += 1 {
subimgs[i].seedTorrent(self.Id)
}
}
func (self *SImage) RemoveFiles() error {
subimgs := ImageSubformatManager.GetAllSubImages(self.Id)
for i := 0; i < len(subimgs); i += 1 {
subimgs[i].StopTorrent()
err := subimgs[i].RemoveFiles()
if err != nil {
return err
}
}
filePath := self.getLocalLocation()
if len(filePath) == 0 {
filePath = self.GetPath("")
}
if len(filePath) > 0 && fileutils2.IsFile(filePath) {
return os.Remove(filePath)
}
return nil
}
func (manager *SImageManager) getAllAliveImages() []SImage {
images := make([]SImage, 0)
q := manager.Query().NotIn("status", api.ImageDeadStatus)
err := db.FetchModelObjects(manager, q, &images)
if err != nil {
log.Errorf("fail to query active images %s", err)
return nil
}
return images
}
func CheckImages() {
images := ImageManager.getAllAliveImages()
for i := 0; i < len(images); i += 1 {
log.Debugf("convert image subformats %s", images[i].Name)
images[i].StartImageCheckTask(context.TODO(), auth.AdminCredential(), "")
}
}
func (self *SImage) AllowGetDetailsSubformats(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return self.IsOwner(userCred) || db.IsAdminAllowGetSpec(userCred, self, "subformats")
}
func (self *SImage) GetDetailsSubformats(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
subimgs := ImageSubformatManager.GetAllSubImages(self.Id)
ret := make([]SImageSubformatDetails, len(subimgs))
for i := 0; i < len(subimgs); i += 1 {
ret[i] = subimgs[i].GetDetails()
}
return jsonutils.Marshal(ret), nil
}
// 磁盘镜像列表
func (manager *SImageManager) ListItemFilter(
ctx context.Context,
q *sqlchemy.SQuery,
userCred mcclient.TokenCredential,
query api.ImageListInput,
) (*sqlchemy.SQuery, error) {
q, err := manager.SSharableVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, query.SharableVirtualResourceListInput)
if err != nil {
return nil, errors.Wrap(err, "SSharableVirtualResourceBaseManager.ListItemFilter")
}
if len(query.DiskFormats) > 0 {
q = q.In("disk_format", query.DiskFormats)
}
if query.Uefi != nil && *query.Uefi {
imagePropertyQ := ImagePropertyManager.Query().
Equals("name", api.IMAGE_UEFI_SUPPORT).Equals("value", "true").SubQuery()
q = q.Join(imagePropertyQ, sqlchemy.Equals(q.Field("id"), imagePropertyQ.Field("image_id")))
}
if query.IsStandard != nil {
if *query.IsStandard {
q = q.IsTrue("is_standard")
} else {
q = q.IsFalse("is_standard")
}
}
if query.Protected != nil {
if *query.Protected {
q = q.IsTrue("protected")
} else {
q = q.IsFalse("protected")
}
}
if query.IsGuestImage != nil {
if *query.IsGuestImage {
q = q.IsTrue("is_guest_image")
} else {
q = q.IsFalse("is_guest_image")
}
}
if query.IsData != nil {
if *query.IsData {
q = q.IsTrue("is_data")
} else {
q = q.IsFalse("is_data")
}
}
return q, nil
}
func (manager *SImageManager) OrderByExtraFields(
ctx context.Context,
q *sqlchemy.SQuery,
userCred mcclient.TokenCredential,
query api.ImageListInput,
) (*sqlchemy.SQuery, error) {
var err error
q, err = manager.SSharableVirtualResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.SharableVirtualResourceListInput)
if err != nil {
return nil, errors.Wrap(err, "SSharableVirtualResourceBaseManager.OrderByExtraFields")
}
return q, nil
}
func (manager *SImageManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
var err error
q, err = manager.SSharableVirtualResourceBaseManager.QueryDistinctExtraField(q, field)
if err == nil {
return q, nil
}
return q, httperrors.ErrNotFound
}
func isActive(localPath string, size int64, chksum string, fastHash string, useFastHash bool) bool {
if len(localPath) == 0 || !fileutils2.Exists(localPath) {
log.Errorf("invalid file: %s", localPath)
return false
}
if size != fileutils2.FileSize(localPath) {
log.Errorf("size mistmatch: %s", localPath)
return false
}
if useFastHash && len(fastHash) > 0 {
fhash, err := fileutils2.FastCheckSum(localPath)
if err != nil {
log.Errorf("IsActive fastChecksum fail %s for %s", err, localPath)
return false
}
if fastHash != fhash {
log.Errorf("IsActive fastChecksum mismatch for %s", localPath)
return false
}
} else {
md5sum, err := fileutils2.MD5(localPath)
if err != nil {
log.Errorf("IsActive md5 fail %s for %s", err, localPath)
return false
}
if chksum != md5sum {
log.Errorf("IsActive checksum mismatch: %s", localPath)
return false
}
}
return true
}
func (self *SImage) IsIso() (error, bool) {
if self.DiskFormat == string(api.ImageTypeISO) {
return nil, true
}
img, err := qemuimg.NewQemuImage(self.GetPath(""))
if err != nil {
return errors.Wrap(err, "open image failed"), false
}
return nil, img.Format == qemuimg.ISO
}
func (self *SImage) isActive(useFast bool) bool {
return isActive(self.getLocalLocation(), self.Size, self.Checksum, self.FastHash, useFast)
}
func (self *SImage) DoCheckStatus(ctx context.Context, userCred mcclient.TokenCredential, useFast bool) {
if utils.IsInStringArray(self.Status, api.ImageDeadStatus) {
return
}
if self.isActive(useFast) {
if self.Status != api.IMAGE_STATUS_ACTIVE {
self.SetStatus(userCred, api.IMAGE_STATUS_ACTIVE, "check active")
}
if len(self.FastHash) == 0 {
fastHash, err := fileutils2.FastCheckSum(self.getLocalLocation())
if err != nil {
log.Errorf("DoCheckStatus fileutils2.FastChecksum fail %s", err)
} else {
_, err := db.Update(self, func() error {
self.FastHash = fastHash
return nil
})
if err != nil {
log.Errorf("DoCheckStatus save FastHash fail %s", err)
}
}
}
img, err := qemuimg.NewQemuImage(self.getLocalLocation())
if err == nil {
format := string(img.Format)
virtualSizeMB := int32(img.SizeBytes / 1024 / 1024)
if (len(format) > 0 && self.DiskFormat != format) || (virtualSizeMB > 0 && self.MinDiskMB != virtualSizeMB) {
db.Update(self, func() error {
if len(format) > 0 {
self.DiskFormat = format
}
if virtualSizeMB > 0 && self.MinDiskMB < virtualSizeMB {
self.MinDiskMB = virtualSizeMB
}
return nil
})
}
} else {
log.Warningf("fail to check image size of %s(%s)", self.Id, self.Name)
}
} else {
if self.Status != api.IMAGE_STATUS_QUEUED {
self.SetStatus(userCred, api.IMAGE_STATUS_QUEUED, "check inactive")
}
}
needConvert := false
subimgs := ImageSubformatManager.GetAllSubImages(self.Id)
// 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 {
subimgs[i].checkStatus(useFast)
if (subimgs[i].Status != api.IMAGE_STATUS_ACTIVE || subimgs[i].TorrentStatus != api.IMAGE_STATUS_ACTIVE) && utils.IsInStringArray(subimgs[i].Format, options.Options.TargetImageFormats) {
needConvert = true
}
}
if self.Status == api.IMAGE_STATUS_ACTIVE {
if needConvert {
log.Infof("Image %s is active and need convert", self.Name)
self.StartImageConvertTask(ctx, userCred, "")
} else if options.Options.EnableTorrentService {
self.seedTorrents()
}
}
}
func (self *SImage) AllowPerformMarkStandard(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
data jsonutils.JSONObject,
) bool {
return db.IsAdminAllowPerform(userCred, self, "mark-standard") && self.IsGuestImage.IsFalse()
}
func (self *SImage) PerformMarkStandard(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
data jsonutils.JSONObject,
) (jsonutils.JSONObject, error) {
if self.IsGuestImage.IsTrue() {
return nil, errors.Wrap(httperrors.ErrForbidden, "cannot mark standard to a guest image")
}
isStandard := jsonutils.QueryBoolean(data, "is_standard", false)
if !self.IsStandard.IsTrue() && isStandard {
input := apis.PerformPublicInput{
Scope: "system",
}
_, err := self.PerformPublic(ctx, userCred, query, input)
if err != nil {
return nil, errors.Wrap(err, "PerformPublic")
}
diff, err := db.Update(self, func() error {
self.IsStandard = tristate.True
return nil
})
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
db.OpsLog.LogEvent(self, db.ACT_UPDATE, diff, userCred)
} else if self.IsStandard.IsTrue() && !isStandard {
diff, err := db.Update(self, func() error {
self.IsStandard = tristate.False
return nil
})
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
db.OpsLog.LogEvent(self, db.ACT_UPDATE, diff, userCred)
}
return nil, nil
}
func (self *SImage) AllowPerformUpdateTorrentStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return self.IsGuestImage.IsFalse()
}
func (self *SImage) PerformUpdateTorrentStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
formatStr, _ := query.GetString("format")
if len(formatStr) == 0 {
return nil, httperrors.NewMissingParameterError("format")
}
subimg := ImageSubformatManager.FetchSubImage(self.Id, formatStr)
if subimg == nil {
return nil, httperrors.NewResourceNotFoundError("format %s not found", formatStr)
}
subimg.SetStatusSeeding(true)
return nil, nil
}
func (self *SImage) CanUpdate(data jsonutils.JSONObject) bool {
dict := data.(*jsonutils.JSONDict)
// Only allow update description for now when Image is part of guest image
return self.IsGuestImage.IsFalse() || (dict.Length() == 1 && dict.Contains("description"))
}
func (img *SImage) GetQuotaKeys() quotas.IQuotaKeys {
keys := SImageQuotaKeys{}
keys.SBaseProjectQuotaKeys = quotas.OwnerIdProjectQuotaKeys(rbacutils.ScopeProject, img.GetOwnerId())
if img.GetImageType() == api.ImageTypeISO {
keys.Type = string(api.ImageTypeISO)
} else {
keys.Type = string(api.ImageTypeTemplate)
}
return keys
}
func imageCreateInput2QuotaKeys(data jsonutils.JSONObject, ownerId mcclient.IIdentityProvider) quotas.IQuotaKeys {
keys := SImageQuotaKeys{}
keys.SBaseProjectQuotaKeys = quotas.OwnerIdProjectQuotaKeys(rbacutils.ScopeProject, ownerId)
format, _ := data.GetString("disk_format")
if format == string(api.ImageTypeISO) {
keys.Type = string(api.ImageTypeISO)
} else if len(format) > 0 {
keys.Type = string(api.ImageTypeTemplate)
}
return keys
}
func (img *SImage) GetUsages() []db.IUsage {
if img.PendingDeleted || img.Deleted {
return nil
}
usage := SQuota{Image: 1}
keys := img.GetQuotaKeys()
usage.SetKeys(keys)
return []db.IUsage{
&usage,
}
}
func (img *SImage) PerformPublic(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.PerformPublicInput) (jsonutils.JSONObject, error) {
if img.IsStandard.IsTrue() {
return nil, errors.Wrap(httperrors.ErrForbidden, "cannot perform public for standard image")
}
if img.IsGuestImage.IsTrue() {
return nil, errors.Wrap(httperrors.ErrForbidden, "cannot perform public for guest image")
}
return img.SSharableVirtualResourceBase.PerformPublic(ctx, userCred, query, input)
}
func (img *SImage) PerformPrivate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.PerformPrivateInput) (jsonutils.JSONObject, error) {
if img.IsStandard.IsTrue() {
return nil, errors.Wrap(httperrors.ErrForbidden, "cannot perform private for standard image")
}
if img.IsGuestImage.IsTrue() {
return nil, errors.Wrap(httperrors.ErrForbidden, "cannot perform private for guest image")
}
return img.SSharableVirtualResourceBase.PerformPrivate(ctx, userCred, query, input)
}