diff --git a/cmd/climc/shell/imageguest.go b/cmd/climc/shell/imageguest.go index f2fa0d2df5..a09a33a9d0 100644 --- a/cmd/climc/shell/imageguest.go +++ b/cmd/climc/shell/imageguest.go @@ -104,7 +104,52 @@ func init() { printObject(image) } return nil - }) + }, + ) + + type GuestImageUpdateOptions struct { + ID string `help:"id of guest image"` + Name string `help:"new name of guest image"` + Protected string `help:"delete protection" choices:"enable|disable"` + OsType string `help:"os type"` + OsDistribution string `help:"os distribution"` + DiskDriver string `help:"disk driver"` + NetDriver string `help:"net driver"` + } + R(&GuestImageUpdateOptions{}, "guest-image-update", "update guest image", func(s *mcclient.ClientSession, + args *GuestImageUpdateOptions) error { + + params := jsonutils.NewDict() + if len(args.Name) > 0 { + params.Add(jsonutils.NewString(args.Name), "name") + } + if len(args.Protected) > 0 { + if args.Protected == "enable" { + params.Add(jsonutils.JSONTrue, "protected") + } else { + params.Add(jsonutils.JSONFalse, "protected") + } + } + properties := jsonutils.NewDict() + if len(args.OsType) > 0 { + properties.Add(jsonutils.NewString(args.OsType), "os_type") + } + if len(args.OsDistribution) > 0 { + properties.Add(jsonutils.NewString(args.OsDistribution), "os_distribution") + } + if len(args.DiskDriver) > 0 { + properties.Add(jsonutils.NewString(args.DiskDriver), "disk_driver") + } + if len(args.NetDriver) > 0 { + properties.Add(jsonutils.NewString(args.NetDriver), "net_driver") + } + params.Add(properties, "properties") + _, err := modules.GuestImages.Update(s, args.ID, params) + if err != nil { + return err + } + return nil + }) type GuestImageOptions struct { ID string `help:"Guest Image id or name"` diff --git a/pkg/cloudcommon/db/opslog.go b/pkg/cloudcommon/db/opslog.go index 17e3f02267..0622bddd25 100644 --- a/pkg/cloudcommon/db/opslog.go +++ b/pkg/cloudcommon/db/opslog.go @@ -247,6 +247,8 @@ const ( ACT_RESTORE = "restore" ACT_CHANGE_CONFIG = "change_config" ACT_RESET_PASSWORD = "reset_password" + + ACT_SUBIMAGE_UPDATE_FAIL = "guest_image_subimages_update_fail" ) type SOpsLogManager struct { diff --git a/pkg/cloudcommon/policy/defaults.go b/pkg/cloudcommon/policy/defaults.go index 8924d67d1c..de84ea0595 100644 --- a/pkg/cloudcommon/policy/defaults.go +++ b/pkg/cloudcommon/policy/defaults.go @@ -332,6 +332,18 @@ var ( Action: PolicyActionGet, Result: rbacutils.Allow, }, + { + Service: "image", + Resource: "guestimages", + Action: PolicyActionList, + Result: rbacutils.Allow, + }, + { + Service: "image", + Resource: "guestimages", + Action: PolicyActionGet, + Result: rbacutils.Allow, + }, { Service: "log", Resource: "actions", diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 46997223a5..36fd15db51 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -4725,7 +4725,11 @@ func (self *SGuest) GetDiskSnapshotsNotInInstanceSnapshots() ([]SSnapshot, error } 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. + // There is no need to check the availability of guest imag if input.Disks is empty + if len(input.Disks) == 0 { + return nil + } + // 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") diff --git a/pkg/image/models/image_guest.go b/pkg/image/models/image_guest.go index 0626e3c5ea..2673738243 100644 --- a/pkg/image/models/image_guest.go +++ b/pkg/image/models/image_guest.go @@ -110,7 +110,6 @@ func (gi *SGuestImage) PostCreate(ctx context.Context, userCred mcclient.TokenCr // 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) @@ -291,6 +290,7 @@ func (self *SGuestImage) getMoreDetails(ctx context.Context, userCred mcclient.T 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") + extra.Add(jsonutils.NewString(image.DiskFormat), "disk_format") continue } dataImages = append(dataImages, sPair{image.Id, image.Name, image.MinDiskMB, image.DiskFormat}) @@ -338,6 +338,23 @@ func (self *SGuestImage) GetExtraDetails(ctx context.Context, userCred mcclient. return self.getMoreDetails(ctx, userCred, query, extra), nil } +func (self *SGuestImage) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, + data jsonutils.JSONObject) { + + if !data.Contains("name") && !data.Contains("properties") { + return + } + params := data.(*jsonutils.JSONDict) + if task, err := taskman.TaskManager.NewTask(ctx, "GuestImageUpdateTask", self, userCred, params, "", "", + nil); err != nil { + + log.Errorf("GusetImage %s fail to start GuestImageUpdateTask", self.Id) + return + } else { + task.ScheduleRun(nil) + } +} + var checkStatus = map[string]int{ api.IMAGE_STATUS_ACTIVE: 1, api.IMAGE_STATUS_QUEUED: 2, diff --git a/pkg/image/models/image_guest_joint.go b/pkg/image/models/image_guest_joint.go index 4a52f43f39..82a7d26b71 100644 --- a/pkg/image/models/image_guest_joint.go +++ b/pkg/image/models/image_guest_joint.go @@ -18,6 +18,7 @@ import ( "context" "yunion.io/x/pkg/errors" + "yunion.io/x/sqlchemy" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/mcclient" @@ -82,7 +83,9 @@ func (gm *SGuestImageJointManager) GetGuestImageByImageId(imageId string) (*SGue return model.(*SGuestImage), nil } -func (gm *SGuestImageJointManager) GetImagesByGuestImageId(guestImageId string) ([]SImage, error) { +func (gm *SGuestImageJointManager) GetImagesByFilter(guestImageId string, + filter func(q *sqlchemy.SQuery) *sqlchemy.SQuery) ([]SImage, error) { + giJoints, err := gm.GetByGuestImageId(guestImageId) if err != nil { return nil, errors.Wrap(err, "get joints of guest and image failed") @@ -95,6 +98,7 @@ func (gm *SGuestImageJointManager) GetImagesByGuestImageId(guestImageId string) imageIds[i] = giJoints[i].ImageId } q := ImageManager.Query().In("id", imageIds) + q = filter(q) images := make([]SImage, 0, len(imageIds)) err = db.FetchModelObjects(ImageManager, q, &images) if err != nil { @@ -103,6 +107,12 @@ func (gm *SGuestImageJointManager) GetImagesByGuestImageId(guestImageId string) return images, nil } +func (gm *SGuestImageJointManager) GetImagesByGuestImageId(guestImageId string) ([]SImage, error) { + return gm.GetImagesByFilter(guestImageId, func(q *sqlchemy.SQuery) *sqlchemy.SQuery { + return q + }) +} + func (gt *SGuestImageJoint) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error { return db.DeleteModel(ctx, userCred, gt) } diff --git a/pkg/image/models/images.go b/pkg/image/models/images.go index 7613b61a78..1aa234dedc 100644 --- a/pkg/image/models/images.go +++ b/pkg/image/models/images.go @@ -554,7 +554,7 @@ 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() { + if self.IsGuestImage.IsTrue() && !self.CanUpdate(data) { return nil, httperrors.NewForbiddenError("image is the part of guest imgae") } appParams := appsrv.AppContextGetParams(ctx) @@ -757,7 +757,7 @@ func (manager *SImageManager) getExpiredPendingDeleteDisks() []SImage { 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("belong_guest_image") + deadline).Limit(options.Options.PendingDeleteMaxCleanBatchSize).IsFalse("is_guest_image") disks := make([]SImage, 0) err := db.FetchModelObjects(ImageManager, q, &disks) @@ -1260,3 +1260,11 @@ func (self *SImage) PerformUpdateTorrentStatus(ctx context.Context, userCred mcc subimg.SetStatusSeeding(true) return nil, nil } + +func (self *SImage) CanUpdate(data jsonutils.JSONObject) bool { + dict := data.(*jsonutils.JSONDict) + if dict.Length() == 1 && !dict.Contains("description") { + return false + } + return true +} diff --git a/pkg/image/tasks/guest_image_update_task.go b/pkg/image/tasks/guest_image_update_task.go new file mode 100644 index 0000000000..c7d4291444 --- /dev/null +++ b/pkg/image/tasks/guest_image_update_task.go @@ -0,0 +1,85 @@ +// 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/sqlchemy" + + "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/util/logclient" +) + +type GuestImageUpdateTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(GuestImageUpdateTask{}) +} + +func (self *GuestImageUpdateTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + guestImage := obj.(*models.SGuestImage) + subImages, err := models.GuestImageJointManager.GetImagesByFilter(guestImage.GetId(), func(q *sqlchemy.SQuery) *sqlchemy.SQuery { + return q.Asc("name") + }) + if err != nil { + self.taskFailed(ctx, guestImage, err.Error()) + } + if self.Params.Contains("name") { + name, _ := self.Params.GetString("name") + for i := range subImages[:len(subImages)-1] { + sub := &subImages[i] + _, err := db.Update(sub, func() error { + sub.Name = fmt.Sprintf("%s-%s-%d", name, "data", i) + return nil + }) + if err != nil { + self.taskFailed(ctx, guestImage, fmt.Sprintf("modify subimage's name failed: %s", err.Error())) + return + } + } + root := &subImages[len(subImages)-1] + _, err := db.Update(root, func() error { + root.Name = fmt.Sprintf("%s-%s", name, "root") + return nil + }) + if err != nil { + self.taskFailed(ctx, guestImage, fmt.Sprintf("modify subimage's name failed: %s", err.Error())) + return + } + } + if self.Params.Contains("properties") { + rootImageId := subImages[len(subImages)-1].GetId() + props, _ := self.Params.Get("properties") + err := models.ImagePropertyManager.SaveProperties(ctx, self.UserCred, rootImageId, props) + if err != nil { + self.taskFailed(ctx, guestImage, fmt.Sprintf("save properties error: %s", err.Error())) + return + } + } + self.SetStageComplete(ctx, nil) +} + +func (self *GuestImageUpdateTask) taskFailed(ctx context.Context, guestImage *models.SGuestImage, reason string) { + db.OpsLog.LogEvent(guestImage, db.ACT_SUBIMAGE_UPDATE_FAIL, reason, self.UserCred) + logclient.AddActionLogWithContext(ctx, guestImage, logclient.ACT_SUBIMAGE_UPDATE, reason, self.UserCred, false) + self.SetStageFailed(ctx, reason) +} diff --git a/pkg/util/logclient/consts.go b/pkg/util/logclient/consts.go index 7d3248d1c6..3c23d89d4c 100644 --- a/pkg/util/logclient/consts.go +++ b/pkg/util/logclient/consts.go @@ -169,4 +169,6 @@ const ( ACT_VM_DISSOCIATE = "解绑虚拟机" ACT_NATGATEWAY_DISSOCIATE = "解绑NAT网关" ACT_LOADBALANCER_DISSOCIATE = "解绑负载均衡" + + ACT_SUBIMAGE_UPDATE = "更新子镜像" )