mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-10-30 18:23:35 +08:00
Merge pull request #2960 from Mjoycarry/feature/guest_image
Feature: Guest Image
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// 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 shell
|
||||
|
||||
import (
|
||||
"yunion.io/x/jsonutils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/options"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
type GuestImageCreateOptions struct {
|
||||
NAME string `help:"Name of guest image"`
|
||||
ImageNumber int `help:"common image number of guest image"`
|
||||
Protected bool `help:"if guest image is protected"`
|
||||
}
|
||||
|
||||
R(&GuestImageCreateOptions{}, "guest-image-create", "Create guest image's metadata", func(s *mcclient.ClientSession,
|
||||
args *GuestImageCreateOptions) error {
|
||||
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.NewString(args.NAME), "name")
|
||||
if args.ImageNumber > 0 {
|
||||
params.Add(jsonutils.NewInt(int64(args.ImageNumber)), "image_number")
|
||||
}
|
||||
if args.Protected {
|
||||
params.Add(jsonutils.JSONTrue, "protected")
|
||||
}
|
||||
ret, err := modules.GuestImages.Create(s, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(ret)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
type GuestImageListOptions struct {
|
||||
options.BaseListOptions
|
||||
|
||||
Name string `help:"Name filter"`
|
||||
}
|
||||
|
||||
R(&GuestImageListOptions{}, "guest-image-list", "List guest images", func(s *mcclient.ClientSession,
|
||||
args *GuestImageListOptions) error {
|
||||
|
||||
params, err := args.Params()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args.Name) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Name), "name")
|
||||
}
|
||||
params.Add(jsonutils.JSONTrue, "details")
|
||||
rets, err := modules.GuestImages.List(s, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printList(rets, modules.GuestImages.GetColumns(s))
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
type GuestImageDeleteOptions struct {
|
||||
ID []string `help:"Image ID or name"`
|
||||
OverridePendingDelete *bool `help:"Delete image directly instead of pending delete" short-token:"f"`
|
||||
}
|
||||
R(&GuestImageDeleteOptions{}, "guest-image-delete", "Delete a image", func(s *mcclient.ClientSession,
|
||||
args *GuestImageDeleteOptions) error {
|
||||
params, err := options.StructToParams(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ret := modules.GuestImages.BatchDeleteWithParam(s, args.ID, params, nil)
|
||||
printBatchResults(ret, modules.GuestImages.GetColumns(s))
|
||||
return nil
|
||||
})
|
||||
|
||||
type GuestImageCancelDeleteOptions struct {
|
||||
ID string `help:"Guest Image id or name"`
|
||||
}
|
||||
R(&GuestImageCancelDeleteOptions{}, "guest-image-cancel-delete", "Cancel pending delete images",
|
||||
func(s *mcclient.ClientSession,
|
||||
args *GuestImageCancelDeleteOptions) error {
|
||||
if image, e := modules.GuestImages.PerformAction(s, args.ID, "cancel-delete", nil); e != nil {
|
||||
return e
|
||||
} else {
|
||||
printObject(image)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
type GuestImageOptions struct {
|
||||
ID string `help:"Guest Image id or name"`
|
||||
}
|
||||
R(&GuestImageOptions{}, "guest-image-mark-protected", "Mark image protected", func(s *mcclient.ClientSession,
|
||||
args *GuestImageOptions) error {
|
||||
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.JSONTrue, "protected")
|
||||
result, err := modules.GuestImages.Update(s, args.ID, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(result)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
R(&GuestImageOptions{}, "guest-image-mark-unprotected", "Mark image protected", func(s *mcclient.ClientSession,
|
||||
args *GuestImageOptions) error {
|
||||
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.JSONFalse, "protected")
|
||||
result, err := modules.GuestImages.Update(s, args.ID, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(result)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
@@ -492,5 +492,4 @@ func init() {
|
||||
printObject(srv)
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -555,6 +555,23 @@ func init() {
|
||||
return nil
|
||||
})
|
||||
|
||||
R(&options.ServerSaveImageOptions{}, "server-save-guest-image",
|
||||
"save root disk and data disks to new images and upload to glance.", func(s *mcclient.ClientSession,
|
||||
opts *options.ServerSaveImageOptions) error {
|
||||
|
||||
params, err := options.StructToParams(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv, err := modules.Servers.PerformAction(s, opts.ID, "save-guest-image", params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(srv)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
type ServerChangeOwnerOptions struct {
|
||||
ID string `help:"Server to change owner" json:"-"`
|
||||
PROJECT string `help:"Project ID or change" json:"tenant"`
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
get:
|
||||
summary: 获取指定主机镜像的详细信息
|
||||
parameters:
|
||||
- $ref: "../parameters/image.yaml#/imageId"
|
||||
responses:
|
||||
200:
|
||||
description: 主机镜像详细信息
|
||||
schema:
|
||||
$ref: "../schemas/guest_image.yaml#/GuestImageResponse"
|
||||
tags:
|
||||
- guestimage
|
||||
|
||||
delete:
|
||||
summary: 删除指定主机镜像
|
||||
parameters:
|
||||
- $ref: "../parameters/image.yaml#/imageId"
|
||||
responses:
|
||||
200:
|
||||
schema:
|
||||
$ref: "../schemas/guest_image.yaml#/GuestImageResponse"
|
||||
tags:
|
||||
- guestimage
|
||||
@@ -0,0 +1,13 @@
|
||||
get:
|
||||
summary: 按指定条件列出主机镜像
|
||||
parameters:
|
||||
- $ref: "../parameters/common.yaml#/offset"
|
||||
- $ref: "../parameters/common.yaml#/limit"
|
||||
|
||||
responses:
|
||||
200:
|
||||
description: 主机镜像列表信息
|
||||
schema:
|
||||
$ref: "../schemas/guest_image.yaml#/GuestImageListResponse"
|
||||
tags:
|
||||
- guestimage
|
||||
@@ -87,6 +87,12 @@ head:
|
||||
type: string
|
||||
#example: 2019-05-30T09:26:41.000000Z
|
||||
description: 镜像最近一次更新时间
|
||||
x-image-meta-is_data:
|
||||
type: boolean
|
||||
description: 是否是数据盘镜像
|
||||
x-image-meta-is_guest_image:
|
||||
type: boolean
|
||||
description: 镜像是否是主机镜像的一部分
|
||||
tags:
|
||||
- images
|
||||
|
||||
|
||||
@@ -212,6 +212,8 @@ paths:
|
||||
$ref: "./instance/revoke-secgroup.yaml"
|
||||
/servers/{instanceId}/save-image:
|
||||
$ref: "./instance/save-image.yaml"
|
||||
/servers/{instanceId}/save-guest-image:
|
||||
$ref: "./instance/save-guest-image.yaml"
|
||||
/servers/{instanceId}/send-keys:
|
||||
$ref: "./instance/send-keys.yaml"
|
||||
/servers/{instanceId}/set-extra-option:
|
||||
@@ -248,6 +250,12 @@ paths:
|
||||
/images/{imageId}/mark-standard:
|
||||
$ref: "./image/image-standard.yaml"
|
||||
|
||||
/guestimages:
|
||||
$ref: "./image/guest-images.yaml"
|
||||
/guestimages/{imageId}:
|
||||
$ref: "./image/guest-image.yaml"
|
||||
|
||||
|
||||
/natgateways:
|
||||
$ref: "./natgateway/natgateways.yaml"
|
||||
/natgateways/{natgatewayId}:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
post:
|
||||
summary: 保存主机镜像
|
||||
parameters:
|
||||
- $ref: '../parameters/instance.yaml#/instanceId'
|
||||
- in: body
|
||||
name: server
|
||||
required: true
|
||||
schema:
|
||||
$ref: '../schemas/instance.yaml#/InstanceSaveImage'
|
||||
responses:
|
||||
200:
|
||||
description: 实例信息
|
||||
schema:
|
||||
$ref: '../schemas/instance.yaml#/InstanceResponse'
|
||||
tags:
|
||||
- instance
|
||||
@@ -0,0 +1,107 @@
|
||||
GuestImageListResponse:
|
||||
type: object
|
||||
properties:
|
||||
limit:
|
||||
type: interger
|
||||
example: 20
|
||||
guestimages:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/GuestImage'
|
||||
total:
|
||||
type: integer
|
||||
example: 124
|
||||
|
||||
GuestImageResponse:
|
||||
type: object
|
||||
properties:
|
||||
guestimage:
|
||||
type: object
|
||||
$ref: '#/GuestImage'
|
||||
|
||||
GuestImage:
|
||||
type: object
|
||||
description: 主机镜像
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: ea27c0ce-5870-49f3-8d57-f53e63f40361
|
||||
description: 镜像ID
|
||||
readOnly: true
|
||||
can_delete:
|
||||
type: boolean
|
||||
example: true
|
||||
description: 镜像是否可以删除
|
||||
can_update:
|
||||
type: boolean
|
||||
example: true
|
||||
description: 镜像是否可以更新
|
||||
created_at:
|
||||
type: string
|
||||
example: 2019-05-30T09:23:16.000000Z
|
||||
description: 镜像创建时间
|
||||
name:
|
||||
type: string
|
||||
example: ubuntu-16.04.5-server-20181117.qcow2
|
||||
description: 镜像名称
|
||||
properties:
|
||||
type: object
|
||||
$ref: './image.yaml#/ImageProperties'
|
||||
protected:
|
||||
type: boolean
|
||||
example: true
|
||||
description: 是否受保护
|
||||
size:
|
||||
type: integer
|
||||
example: 792592384
|
||||
description: 主机镜像实际大小(系统盘镜像和数据盘镜像的大小之和),单位Byte
|
||||
min_ram_size:
|
||||
type: integer
|
||||
example: 2048
|
||||
description: 创建vm时的最低内存限制
|
||||
root_image:
|
||||
type: object
|
||||
$ref: '#/SubImageInfo'
|
||||
description: 系统盘镜像
|
||||
data_images:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/SubImageInfo'
|
||||
description: 数据盘镜像列表
|
||||
status:
|
||||
type: string
|
||||
example: active
|
||||
enum: [queued,saving,active,converting]
|
||||
description: 镜像状态, queued(队列中), saving(保存中), active(可用)
|
||||
tenant:
|
||||
type: string
|
||||
example: system
|
||||
description: 所属项目名称
|
||||
tenant_id:
|
||||
type: string
|
||||
example: a7f2e2a81a1e4850a41eae5f140ceb14
|
||||
description: 所属项目ID
|
||||
updated_at:
|
||||
type: string
|
||||
example: 2019-05-30T09:26:41.000000Z
|
||||
description: 最近一次更新时间
|
||||
|
||||
SubImageInfo:
|
||||
type: object
|
||||
description: 子镜像信息
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: fasd2344-5470-32f3-4d47-f53323f40361
|
||||
description: 子镜像的ID
|
||||
name:
|
||||
type: string
|
||||
example: root
|
||||
description: 子镜像的Name
|
||||
min_disk_mb:
|
||||
type: integer
|
||||
example: 40960
|
||||
description: 磁盘最低大小限制(MB)
|
||||
disk_format:
|
||||
type: string
|
||||
description: 磁盘格式
|
||||
@@ -100,6 +100,14 @@ Image:
|
||||
type: string
|
||||
example: 2019-05-30T09:26:41.000000Z
|
||||
description: 最近一次更新时间
|
||||
is_guest_image:
|
||||
type: boolean
|
||||
example: true
|
||||
description: 是否是主机镜像的一部分
|
||||
is_data:
|
||||
type: boolean
|
||||
example: true
|
||||
description: 是否是数据盘的镜像
|
||||
|
||||
|
||||
ImageProperties:
|
||||
|
||||
@@ -657,6 +657,9 @@ InstanceCreate:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/InstanceDisk'
|
||||
guest_image_id:
|
||||
type: string
|
||||
example: 如果使用主机镜像创建主机,需要额外指定主机镜像的 ID,来检验 disks 的 Image ID 是否正确
|
||||
|
||||
|
||||
InstanceDisk:
|
||||
@@ -1053,6 +1056,11 @@ Instance:
|
||||
items:
|
||||
$ref: '#/InstanceDiskInfo'
|
||||
description: 实例盘列表
|
||||
disk_count:
|
||||
type: integer
|
||||
example: 4
|
||||
description: 磁盘数量
|
||||
|
||||
|
||||
InstanceListResponse:
|
||||
type: object
|
||||
|
||||
@@ -202,6 +202,9 @@ type ServerCreateInput struct {
|
||||
ParentTaskId string `json:"__parent_task_id,omitempty"`
|
||||
// default stroage type if host is given
|
||||
DefaultStorageType string `json:"default_storage_type,omitempty"`
|
||||
|
||||
// Guest Image ID
|
||||
GuestImageID string
|
||||
}
|
||||
|
||||
type ServerCloneInput struct {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 compute
|
||||
|
||||
type sPair struct {
|
||||
ID string
|
||||
Name string
|
||||
MinDiskMB int32
|
||||
DiskFormat string
|
||||
}
|
||||
|
||||
type SImagesInGuest struct {
|
||||
RootImage sPair
|
||||
DataImages []sPair
|
||||
}
|
||||
@@ -80,11 +80,12 @@ const (
|
||||
ACT_SPLIT = "net_split"
|
||||
ACT_MERGE = "net_merge"
|
||||
|
||||
ACT_SAVING = "saving"
|
||||
ACT_SAVE = "save"
|
||||
ACT_SAVE_FAIL = "save_fail"
|
||||
ACT_PROBE = "probe"
|
||||
ACT_PROBE_FAIL = "probe_fail"
|
||||
ACT_SAVING = "saving"
|
||||
ACT_SAVE = "save"
|
||||
ACT_SAVE_FAIL = "save_fail"
|
||||
ACT_PROBE = "probe"
|
||||
ACT_PROBE_FAIL = "probe_fail"
|
||||
ACT_IMAGE_DELETE_FAIL = "delete_fail"
|
||||
|
||||
ACT_SWITCHED = "switched"
|
||||
ACT_SWITCH_FAILED = "switch_failed"
|
||||
@@ -216,6 +217,8 @@ const (
|
||||
ACT_GUEST_ATTACH_ISOLATED_DEVICE_FAIL = "guest_attach_isolated_deivce_fail"
|
||||
ACT_GUEST_DETACH_ISOLATED_DEVICE = "guest_detach_isolated_deivce"
|
||||
ACT_GUEST_DETACH_ISOLATED_DEVICE_FAIL = "guest_detach_isolated_deivce_fail"
|
||||
ACT_GUEST_SAVE_GUEST_IMAGE = "guest_save_guest_image"
|
||||
ACT_GUEST_SAVE_GUEST_IMAGE_FAIL = "guest_save_guest_image_fail"
|
||||
|
||||
ACT_CHANGE_BANDWIDTH = "eip_change_bandwidth"
|
||||
|
||||
|
||||
@@ -473,6 +473,12 @@ func (self *SBaremetalGuestDriver) StartGuestSaveImage(ctx context.Context, user
|
||||
return httperrors.NewUnsupportOperationError("Cannot save image for baremtal")
|
||||
}
|
||||
|
||||
func (self *SBaremetalGuestDriver) StartGuestSaveGuestImage(ctx context.Context, userCred mcclient.TokenCredential,
|
||||
guest *models.SGuest, params *jsonutils.JSONDict, parentTaskId string) error {
|
||||
|
||||
return httperrors.NewUnsupportOperationError("Cannot save image for baremtal")
|
||||
}
|
||||
|
||||
func (self *SBaremetalGuestDriver) StartGuestResetTask(guest *models.SGuest, ctx context.Context, userCred mcclient.TokenCredential, isHard bool, parentTaskId string) error {
|
||||
task, err := taskman.TaskManager.NewTask(ctx, "BaremetalServerResetTask", guest, userCred, nil, "", parentTaskId, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -297,3 +297,15 @@ func (self *SVirtualizedGuestDriver) StartGuestSaveImage(ctx context.Context, us
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SVirtualizedGuestDriver) StartGuestSaveGuestImage(ctx context.Context, userCred mcclient.TokenCredential,
|
||||
guest *models.SGuest, params *jsonutils.JSONDict, parentTaskId string) error {
|
||||
|
||||
if task, err := taskman.TaskManager.NewTask(ctx, "GuestSaveGuestImageTask", guest, userCred, params, parentTaskId,
|
||||
"", nil); err != nil {
|
||||
return err
|
||||
} else {
|
||||
task.ScheduleRun(nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,10 +24,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/errors"
|
||||
"yunion.io/x/pkg/tristate"
|
||||
"yunion.io/x/pkg/util/fileutils"
|
||||
"yunion.io/x/pkg/util/regutils"
|
||||
@@ -187,6 +186,79 @@ func (self *SGuest) StartGuestSaveImage(ctx context.Context, userCred mcclient.T
|
||||
return self.GetDriver().StartGuestSaveImage(ctx, userCred, self, data, parentTaskId)
|
||||
}
|
||||
|
||||
func (self *SGuest) AllowPerformSaveGuestImage(ctx context.Context, userCred mcclient.TokenCredential,
|
||||
query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
|
||||
|
||||
return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "save-guest-image")
|
||||
}
|
||||
|
||||
func (self *SGuest) PerformSaveGuestImage(ctx context.Context, userCred mcclient.TokenCredential,
|
||||
query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
|
||||
if !utils.IsInStringArray(self.Status, []string{api.VM_READY}) {
|
||||
return nil, httperrors.NewBadRequestError("Cannot save image in status %s", self.Status)
|
||||
}
|
||||
if !data.Contains("name") {
|
||||
return nil, httperrors.NewMissingParameterError("Image name is required")
|
||||
}
|
||||
if self.Hypervisor != api.HYPERVISOR_KVM {
|
||||
return nil, httperrors.NewBadRequestError("Support only by KVM Hypervisor")
|
||||
}
|
||||
disks := self.CategorizeDisks()
|
||||
|
||||
if disks.Root == nil {
|
||||
return nil, httperrors.NewInternalServerError("No root image")
|
||||
}
|
||||
|
||||
// build images
|
||||
images := jsonutils.NewArray()
|
||||
diskList := append(disks.Data, disks.Root)
|
||||
for _, disk := range diskList {
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.NewString(disk.DiskFormat), "disk_format")
|
||||
params.Add(jsonutils.NewInt(int64(disk.DiskSize)), "virtual_size")
|
||||
images.Add(params)
|
||||
}
|
||||
|
||||
// build parameters
|
||||
kwargs := data.(*jsonutils.JSONDict)
|
||||
|
||||
kwargs.Add(jsonutils.NewInt(int64(len(disks.Data)+1)), "image_number")
|
||||
properties := jsonutils.NewDict()
|
||||
if notes, err := kwargs.GetString("notes"); err != nil && len(notes) > 0 {
|
||||
properties.Add(jsonutils.NewString(notes), "notes")
|
||||
}
|
||||
osType := self.OsType
|
||||
if len(osType) == 0 {
|
||||
osType = "Linux"
|
||||
}
|
||||
properties.Add(jsonutils.NewString(osType), "os_type")
|
||||
kwargs.Add(properties, "properties")
|
||||
|
||||
kwargs.Add(images, "images")
|
||||
|
||||
s := auth.GetAdminSession(ctx, options.Options.Region, "")
|
||||
ret, err := modules.GuestImages.Create(s, kwargs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageIds, err := ret.Get("image_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("something wrong in glance")
|
||||
}
|
||||
tmp := imageIds.(*jsonutils.JSONArray)
|
||||
if tmp.Length() != len(disks.Data)+1 {
|
||||
return nil, fmt.Errorf("create subimage of guest image error")
|
||||
}
|
||||
taskParams := jsonutils.NewDict()
|
||||
taskParams.Add(imageIds, "image_ids")
|
||||
return nil, self.StartGuestSaveGuestImage(ctx, userCred, taskParams, "")
|
||||
}
|
||||
|
||||
func (self *SGuest) StartGuestSaveGuestImage(ctx context.Context, userCred mcclient.TokenCredential, data *jsonutils.JSONDict, parentTaskId string) error {
|
||||
return self.GetDriver().StartGuestSaveGuestImage(ctx, userCred, self, data, parentTaskId)
|
||||
}
|
||||
|
||||
func (self *SGuest) AllowPerformSync(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
|
||||
return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "sync")
|
||||
|
||||
|
||||
@@ -112,6 +112,8 @@ type IGuestDriver interface {
|
||||
|
||||
StartGuestSaveImage(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, params *jsonutils.JSONDict, parentTaskId string) error
|
||||
|
||||
StartGuestSaveGuestImage(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, params *jsonutils.JSONDict, parentTaskId string) error
|
||||
|
||||
RequestStopGuestForDelete(ctx context.Context, guest *SGuest, host *SHost, task taskman.ITask) error
|
||||
|
||||
RequestDetachDisksFromGuestForDelete(ctx context.Context, guest *SGuest, task taskman.ITask) error
|
||||
|
||||
@@ -54,6 +54,7 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
||||
"yunion.io/x/onecloud/pkg/util/billing"
|
||||
"yunion.io/x/onecloud/pkg/util/logclient"
|
||||
"yunion.io/x/onecloud/pkg/util/netutils2"
|
||||
@@ -927,6 +928,12 @@ func (manager *SGuestManager) validateCreateData(
|
||||
input.InstanceGroupIds = newGroupIds
|
||||
}
|
||||
|
||||
// check that all image of disk is the part of guest imgae, if use guest image to create guest
|
||||
err = manager.checkGuestImage(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var hypervisor string
|
||||
// var rootStorageType string
|
||||
var osProf osprofile.SOSProfile
|
||||
@@ -1529,6 +1536,10 @@ func (self *SGuest) moreExtraInfo(extra *jsonutils.JSONDict, fields stringutils2
|
||||
extra.Add(jsonutils.Marshal(metaData), "metadata")
|
||||
}
|
||||
|
||||
q := self.GetDisksQuery()
|
||||
count, _ := q.CountWithError()
|
||||
extra.Add(jsonutils.NewInt(int64(count)), "disk_count")
|
||||
|
||||
return extra
|
||||
}
|
||||
|
||||
@@ -4755,3 +4766,51 @@ func (self *SGuest) GetDiskSnapshotsNotInInstanceSnapshots() ([]SSnapshot, error
|
||||
}
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
func (self *SGuestManager) checkGuestImage(ctx context.Context, input *api.ServerCreateInput) error {
|
||||
// that data disks has image id show that these image is part of guest image.
|
||||
for _, config := range input.Disks[1:] {
|
||||
if len(config.ImageId) != 0 && len(input.GuestImageID) == 0 {
|
||||
return httperrors.NewMissingParameterError("guest_image_id")
|
||||
}
|
||||
}
|
||||
|
||||
if len(input.GuestImageID) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
guestImageId := input.GuestImageID
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.JSONTrue, "details")
|
||||
|
||||
s := auth.GetAdminSession(ctx, options.Options.Region, "")
|
||||
ret, err := modules.GuestImages.Get(s, guestImageId, params)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get guest image from glance error")
|
||||
}
|
||||
|
||||
images := &api.SImagesInGuest{}
|
||||
err = ret.Unmarshal(images)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get guest image from glance error")
|
||||
}
|
||||
imageIdMap := make(map[string]struct{})
|
||||
for _, pair := range images.DataImages {
|
||||
imageIdMap[pair.ID] = struct{}{}
|
||||
}
|
||||
imageIdMap[images.RootImage.ID] = struct{}{}
|
||||
|
||||
// check
|
||||
for _, diskConfig := range input.Disks {
|
||||
if len(diskConfig.ImageId) != 0 {
|
||||
if _, ok := imageIdMap[diskConfig.ImageId]; !ok {
|
||||
return httperrors.NewBadRequestError("image %s do not belong to guest image %s", diskConfig.ImageId, guestImageId)
|
||||
}
|
||||
delete(imageIdMap, diskConfig.ImageId)
|
||||
}
|
||||
}
|
||||
if len(imageIdMap) != 0 {
|
||||
return httperrors.NewBadRequestError("miss some subimage of guest image")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/compute/models"
|
||||
"yunion.io/x/onecloud/pkg/util/logclient"
|
||||
)
|
||||
|
||||
type GuestSaveGuestImageTask struct {
|
||||
SGuestBaseTask
|
||||
}
|
||||
|
||||
func init() {
|
||||
taskman.RegisterTask(GuestSaveGuestImageTask{})
|
||||
}
|
||||
|
||||
func (self *GuestSaveGuestImageTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
|
||||
// prepare save image
|
||||
guest := obj.(*models.SGuest)
|
||||
|
||||
self.SetStage("OnSaveRootImageComplete", nil)
|
||||
disks := guest.CategorizeDisks()
|
||||
imageIds, _ := self.Params.GetArray("image_ids")
|
||||
self.Params.Remove("image_ids")
|
||||
|
||||
// data disk
|
||||
for index, dataDisk := range disks.Data {
|
||||
params := jsonutils.DeepCopy(self.Params).(*jsonutils.JSONDict)
|
||||
params.Add(imageIds[index], "image_id")
|
||||
if err := dataDisk.StartDiskSaveTask(ctx, self.UserCred, params, self.GetTaskId()); err != nil {
|
||||
self.taskFailed(ctx, guest, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
self.Params.Add(imageIds[len(imageIds)-1], "image_id")
|
||||
if err := disks.Root.StartDiskSaveTask(ctx, self.UserCred, self.Params, self.GetTaskId()); err != nil {
|
||||
self.taskFailed(ctx, guest, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GuestSaveGuestImageTask) OnSaveRootImageComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) {
|
||||
subTasks := taskman.SubTaskManager.GetTotalSubtasks(self.Id, "on_save_root_image_complete", taskman.SUBTASK_FAIL)
|
||||
|
||||
if len(subTasks) > 0 {
|
||||
self.taskFailed(ctx, guest, "subtask failed")
|
||||
}
|
||||
|
||||
if restart, _ := self.GetParams().Bool("restart"); restart {
|
||||
self.SetStage("on_start_server_complete", nil)
|
||||
guest.StartGueststartTask(ctx, self.GetUserCred(), nil, self.GetTaskId())
|
||||
} else {
|
||||
self.taskSuc(ctx, guest)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GuestSaveGuestImageTask) OnSaveRootImageCompleteFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) {
|
||||
log.Errorf("Guest save image failed: %s", data.PrettyString())
|
||||
self.taskFailed(ctx, guest, "")
|
||||
}
|
||||
|
||||
func (self *GuestSaveGuestImageTask) OnStartServerComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) {
|
||||
self.taskSuc(ctx, guest)
|
||||
}
|
||||
|
||||
func (self *GuestSaveGuestImageTask) OnStartServerCompleteFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) {
|
||||
// even if start server failed, the task that save guest image is successful
|
||||
self.taskSuc(ctx, guest)
|
||||
}
|
||||
|
||||
func (self *GuestSaveGuestImageTask) taskSuc(ctx context.Context, guest *models.SGuest) {
|
||||
self.SetStageComplete(ctx, nil)
|
||||
}
|
||||
|
||||
func (self *GuestSaveGuestImageTask) taskFailed(ctx context.Context, guest *models.SGuest, reason string) {
|
||||
|
||||
guest.SetStatus(self.UserCred, api.VM_SAVE_DISK_FAILED, reason)
|
||||
db.OpsLog.LogEvent(guest, db.ACT_GUEST_SAVE_GUEST_IMAGE_FAIL, reason, self.UserCred)
|
||||
logclient.AddActionLogWithStartable(self, guest, logclient.ACT_IMAGE_SAVE, reason, self.UserCred, false)
|
||||
|
||||
self.SetStageFailed(ctx, reason)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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 modules
|
||||
|
||||
import "yunion.io/x/onecloud/pkg/mcclient/modulebase"
|
||||
|
||||
var GuestImages modulebase.ResourceManager
|
||||
|
||||
func init() {
|
||||
GuestImages = NewImageManager("guestimage", "guestimages",
|
||||
[]string{"ID", "Name", "Status", "Size"},
|
||||
[]string{})
|
||||
register(&GuestImages)
|
||||
}
|
||||
@@ -283,6 +283,8 @@ type ServerCreateOptions struct {
|
||||
EipBw int `help:"allocate EIP with bandwidth in MB when server is created" json:"eip_bw,omitzero"`
|
||||
EipChargeType string `help:"newly allocated EIP charge type" choices:"traffic|bandwidth" json:"eip_charge_type,omitempty"`
|
||||
Eip string `help:"associate with an existing EIP when server is created" json:"eip,omitempty"`
|
||||
|
||||
GuestImageID string `help:"create from guest image, need to specify the guest image id"`
|
||||
}
|
||||
|
||||
func (o *ServerCreateOptions) ToScheduleInput() (*schedapi.ScheduleInput, error) {
|
||||
@@ -362,6 +364,7 @@ func (opts *ServerCreateOptions) Params() (*computeapi.ServerCreateInput, error)
|
||||
Eip: opts.Eip,
|
||||
EnableCloudInit: opts.EnableCloudInit,
|
||||
OsType: opts.OsType,
|
||||
GuestImageID: opts.GuestImageID,
|
||||
}
|
||||
|
||||
if opts.GenerateName {
|
||||
|
||||
Reference in New Issue
Block a user