From 335cdf37b4b2bcc2223c87e408ae67162406a1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=88=E8=BD=A9?= Date: Thu, 5 Feb 2026 16:40:40 +0800 Subject: [PATCH] fix(region): snapshot policy operation (#24251) --- cmd/climc/shell/compute/disks.go | 57 ++-------- pkg/apis/compute/cachedimage.go | 2 +- pkg/apis/compute/disk.go | 24 +++-- pkg/apis/compute/guests.go | 13 +++ pkg/compute/models/disks.go | 100 ++++++++++++++++-- pkg/compute/models/guest_actions.go | 90 ++++++++++++++++ pkg/compute/models/guest_queries.go | 37 +++++++ pkg/compute/models/guests.go | 10 ++ .../models/snapshot_policy_resources.go | 5 + pkg/compute/models/snapshotpolicy.go | 58 +++++++++- pkg/mcclient/options/compute/disks.go | 34 +++--- pkg/mcclient/options/compute/servers.go | 6 +- 12 files changed, 352 insertions(+), 84 deletions(-) diff --git a/cmd/climc/shell/compute/disks.go b/cmd/climc/shell/compute/disks.go index 993967f18e..5a15cbdfee 100644 --- a/cmd/climc/shell/compute/disks.go +++ b/cmd/climc/shell/compute/disks.go @@ -42,32 +42,18 @@ import ( func init() { cmd := shell.NewResourceCmd(&modules.Disks) cmd.List(&compute_options.DiskListOptions{}) + cmd.Show(&compute_options.DiskIdOptions{}) + cmd.Perform("public", &compute_options.DiskIdOptions{}) + cmd.Perform("private", &compute_options.DiskIdOptions{}) + cmd.Perform("syncstatus", &compute_options.DiskIdOptions{}) + cmd.Perform("change-owner-candidate-domains", &compute_options.DiskIdOptions{}) + cmd.Perform("disk-cancel-delete", &compute_options.DiskIdOptions{}) cmd.Perform("set-class-metadata", &options.ResourceMetadataOptions{}) cmd.Perform("rebuild", &options.ResourceIdOptions{}) cmd.Perform("migrate", &compute_options.DiskMigrateOptions{}) cmd.Perform("change-billing-type", new(compute_options.DiskChangeBillingTypeOptions)) cmd.Perform("change-storage-type", &compute_options.DiskChangeStorageTypeOptions{}) - type DiskDetailOptions struct { - ID string `help:"ID or Name of disk"` - } - R(&DiskDetailOptions{}, "disk-show", "Show details of disk", func(s *mcclient.ClientSession, args *DiskDetailOptions) error { - disk, e := modules.Disks.Get(s, args.ID, nil) - if e != nil { - return e - } - printObject(disk) - return nil - }) - R(&DiskDetailOptions{}, "disk-cancel-delete", "Cancel pending delete disks", func(s *mcclient.ClientSession, args *DiskDetailOptions) error { - disk, e := modules.Disks.PerformAction(s, args.ID, "cancel-delete", nil) - if e != nil { - return e - } - printObject(disk) - return nil - }) - type DiskDeleteOptions struct { ID []string `help:"ID of disks to delete" metavar:"DISK"` OverridePendingDelete bool `help:"Delete disk directly instead of pending delete" short-token:"f"` @@ -96,25 +82,7 @@ func init() { return nil }) - R(&DiskDetailOptions{}, "disk-public", "Make a disk public", func(s *mcclient.ClientSession, args *DiskDetailOptions) error { - disk, e := modules.Disks.PerformAction(s, args.ID, "public", nil) - if e != nil { - return e - } - printObject(disk) - return nil - }) - - R(&DiskDetailOptions{}, "disk-private", "Make a disk private", func(s *mcclient.ClientSession, args *DiskDetailOptions) error { - disk, e := modules.Disks.PerformAction(s, args.ID, "private", nil) - if e != nil { - return e - } - printObject(disk) - return nil - }) - - R(&DiskDetailOptions{}, "disk-metadata", "Get metadata of a disk", func(s *mcclient.ClientSession, args *DiskDetailOptions) error { + R(&compute_options.DiskIdOptions{}, "disk-metadata", "Get metadata of a disk", func(s *mcclient.ClientSession, args *compute_options.DiskIdOptions) error { meta, e := modules.Disks.GetMetadata(s, args.ID, nil) if e != nil { return e @@ -123,15 +91,6 @@ func init() { return nil }) - R(&DiskDetailOptions{}, "disk-syncstatus", "Sync status for disk", func(s *mcclient.ClientSession, args *DiskDetailOptions) error { - ret, e := modules.Disks.PerformAction(s, args.ID, "syncstatus", nil) - if e != nil { - return e - } - printObject(ret) - return nil - }) - type DiskUpdateOptions struct { ID string `help:"ID or name of disk"` Name string `help:"New name of disk"` @@ -300,7 +259,7 @@ func init() { return nil }) - R(&DiskDetailOptions{}, "disk-change-owner-candidate-domains", "Get change owner candidate domain list", func(s *mcclient.ClientSession, args *DiskDetailOptions) error { + R(&compute_options.DiskIdOptions{}, "disk-change-owner-candidate-domains", "Get change owner candidate domain list", func(s *mcclient.ClientSession, args *compute_options.DiskIdOptions) error { result, err := modules.Disks.GetSpecific(s, args.ID, "change-owner-candidate-domains", nil) if err != nil { return err diff --git a/pkg/apis/compute/cachedimage.go b/pkg/apis/compute/cachedimage.go index b64cf89fb3..43e7ee8ca0 100644 --- a/pkg/apis/compute/cachedimage.go +++ b/pkg/apis/compute/cachedimage.go @@ -87,6 +87,6 @@ type CachedimageListInput struct { // valid cachedimage Valid bool `json:"valid"` - // enum: [public, private] + // enum: ["public", "private"] CloudEnv string `json:"cloud_env"` } diff --git a/pkg/apis/compute/disk.go b/pkg/apis/compute/disk.go index 1f6b412f11..abc290a653 100644 --- a/pkg/apis/compute/disk.go +++ b/pkg/apis/compute/disk.go @@ -169,6 +169,8 @@ type DiskListInput struct { // 根据是否绑定快照策略过滤 BindingSnapshotpolicy *bool `json:"binding_snapshotpolicy"` + // 根据是否磁盘所在虚拟机是否绑定主机快照策略 + BindingServerSnapshotpolicy *bool `json:"binding_server_snapshotpolicy"` } type DiskResourceInput struct { @@ -213,13 +215,22 @@ type SimpleGuest struct { Bps int `json:"bps"` // 计费类型 BillingType string `json:"billing_type"` + + // 磁盘绑定的快照策略列表 + Snapshotpolicy []SimpleSnapshotPolicy `json:"snapshotpolicy"` } type SimpleSnapshotPolicy struct { - Id string `json:"id"` - Name string `json:"name"` - RepeatWeekdays []int `json:"repeat_weekdays"` - TimePoints []int `json:"time_points"` + // 快照策略ID + Id string `json:"id"` + // 快照策略名称 + Name string `json:"name"` + // 快照策略类型 + ResourceType string `json:"resource_type"` + // 快照策略重复周期 + RepeatWeekdays []int `json:"repeat_weekdays"` + // 快照策略时间点 + TimePoints []int `json:"time_points"` } type DiskDetails struct { @@ -239,11 +250,11 @@ type DiskDetails struct { GuestStatus string `json:"guest_status"` // 所挂载虚拟机计费类型 GuestBillingType string `json:"guest_billing_type"` + // 磁盘所在虚拟机绑定的主机快照策略数量 + GuestSnapshotpolicyCount int `json:"guest_snapshotpolicy_count"` // 自动清理时间 AutoDeleteAt time.Time `json:"auto_delete_at"` - // 自动快照策略状态 - SnapshotpolicyStatus string `json:"snapshotpolicy_status,allowempty"` // 自动快照策略 Snapshotpolicies []SimpleSnapshotPolicy `json:"snapshotpolicies"` @@ -351,6 +362,7 @@ type DiskChagneStorageTypeInput struct { } type DiskSnapshotpolicyInput struct { + // 快照策略ID SnapshotpolicyId string `json:"snapshotpolicy_id"` } diff --git a/pkg/apis/compute/guests.go b/pkg/apis/compute/guests.go index 6bf1550ed3..4eaf8e644f 100644 --- a/pkg/apis/compute/guests.go +++ b/pkg/apis/compute/guests.go @@ -134,6 +134,14 @@ type ServerListInput struct { // 根据是否绑定快照策略过滤 BindingSnapshotpolicy *bool `json:"binding_snapshotpolicy"` + // 根据虚机关联的磁盘是否绑定快照策略过滤 + BindingDisksSnapshotpolicy *bool `json:"binding_disks_snapshotpolicy"` +} + +// 主机快照策略绑定/设置接口入参 +type ServerSnapshotpolicyInput struct { + // 快照策略ID + SnapshotpolicyId string `json:"snapshotpolicy_id"` } func (input *ServerListInput) AfterUnmarshal() { @@ -183,6 +191,11 @@ type ServerDetails struct { // 磁盘概要 Disks string `json:"disks"` + // 主机快照策略数量 + SnapshotpolicyCount int `json:"snapshotpolicy_count"` + // 磁盘快照策略数量 + DisksSnapshotpolicyCount int `json:"disks_snapshotpolicy_count"` + // 磁盘详情 DisksInfo []GuestDiskInfo `json:"disks_info"` // 虚拟机Ip列表 diff --git a/pkg/compute/models/disks.go b/pkg/compute/models/disks.go index ef24c1b3f9..0298d541e4 100644 --- a/pkg/compute/models/disks.go +++ b/pkg/compute/models/disks.go @@ -218,6 +218,16 @@ func (manager *SDiskManager) ListItemFilter( q = q.NotIn("id", spjsq) } } + if query.BindingServerSnapshotpolicy != nil { + guestDisks := GuestdiskManager.Query("disk_id") + sq := SnapshotPolicyResourceManager.Query("resource_id").Equals("resource_type", api.SNAPSHOT_POLICY_TYPE_SERVER).SubQuery() + gdsq := guestDisks.Join(sq, sqlchemy.Equals(guestDisks.Field("guest_id"), sq.Field("resource_id"))).SubQuery() + if *query.BindingServerSnapshotpolicy { + q = q.In("id", gdsq) + } else { + q = q.NotIn("id", gdsq) + } + } guestId := query.ServerId if len(guestId) > 0 { @@ -2550,11 +2560,12 @@ func (manager *SDiskManager) FetchCustomizeColumns( return rows } - guests := map[string][]api.SimpleGuest{} + guests, guestIds := map[string][]api.SimpleGuest{}, []string{} for _, guest := range guestInfo { _, ok := guests[guest.DiskId] if !ok { guests[guest.DiskId] = []api.SimpleGuest{} + guestIds = append(guestIds, guest.Id) } guests[guest.DiskId] = append(guests[guest.DiskId], api.SimpleGuest{ Id: guest.Id, @@ -2571,7 +2582,7 @@ func (manager *SDiskManager) FetchCustomizeColumns( } policySQ := SnapshotPolicyManager.Query().SubQuery() - dps := SnapshotPolicyResourceManager.Query().Equals("resource_type", api.SNAPSHOT_POLICY_TYPE_DISK).SubQuery() + dps := SnapshotPolicyResourceManager.Query().SubQuery() q = policySQ.Query( policySQ.Field("id"), @@ -2579,8 +2590,9 @@ func (manager *SDiskManager) FetchCustomizeColumns( policySQ.Field("time_points"), policySQ.Field("repeat_weekdays"), dps.Field("resource_id"), + dps.Field("resource_type"), ).Join(dps, sqlchemy.Equals(dps.Field("snapshotpolicy_id"), policySQ.Field("id"))). - Filter(sqlchemy.In(dps.Field("resource_id"), diskIds)) + Filter(sqlchemy.OR(sqlchemy.In(dps.Field("resource_id"), diskIds), sqlchemy.In(dps.Field("resource_id"), guestIds))) policyInfo := []struct { Id string @@ -2588,7 +2600,8 @@ func (manager *SDiskManager) FetchCustomizeColumns( Status string TimePoints []int RepeatWeekdays []int - DiskId string + ResourceId string + ResourceType string }{} err = q.All(&policyInfo) if err != nil { @@ -2598,15 +2611,16 @@ func (manager *SDiskManager) FetchCustomizeColumns( policies := map[string][]api.SimpleSnapshotPolicy{} for _, policy := range policyInfo { - _, ok := policies[policy.DiskId] + _, ok := policies[policy.ResourceId] if !ok { - policies[policy.DiskId] = []api.SimpleSnapshotPolicy{} + policies[policy.ResourceId] = []api.SimpleSnapshotPolicy{} } - policies[policy.DiskId] = append(policies[policy.DiskId], api.SimpleSnapshotPolicy{ + policies[policy.ResourceId] = append(policies[policy.ResourceId], api.SimpleSnapshotPolicy{ Id: policy.Id, Name: policy.Name, RepeatWeekdays: policy.RepeatWeekdays, TimePoints: policy.TimePoints, + ResourceType: policy.ResourceType, }) } @@ -2614,12 +2628,15 @@ func (manager *SDiskManager) FetchCustomizeColumns( rows[i].Guests, _ = guests[diskIds[i]] names, status, billingTypes := []string{}, []string{}, []string{} var iops, bps int - for _, guest := range rows[i].Guests { + for j := range rows[i].Guests { + guest := rows[i].Guests[j] names = append(names, guest.Name) status = append(status, guest.Status) iops = guest.Iops bps = guest.Bps billingTypes = append(billingTypes, guest.BillingType) + rows[i].Guests[j].Snapshotpolicy, _ = policies[guest.Id] + rows[i].GuestSnapshotpolicyCount += len(rows[i].Guests[j].Snapshotpolicy) } rows[i].GuestCount = len(rows[i].Guests) rows[i].Guest = strings.Join(names, ",") @@ -3160,12 +3177,33 @@ func (disk *SDisk) GetUsages() []db.IUsage { } } +// 绑定磁盘快照策略 +// 磁盘只能绑定一个快照策略,已绑定时报错 +// 若磁盘所属主机已绑定主机快照策略,则磁盘不能再绑定快照策略 func (disk *SDisk) PerformBindSnapshotpolicy( ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input *api.DiskSnapshotpolicyInput, ) (jsonutils.JSONObject, error) { + // 磁盘只能绑定一个快照策略,已绑定时报错 + cnt, err := SnapshotPolicyResourceManager.GetBindingCount(disk.Id, api.SNAPSHOT_POLICY_TYPE_DISK) + if err != nil { + return nil, errors.Wrap(err, "GetBindingCount") + } + if cnt > 0 { + return nil, httperrors.NewConflictError("disk already bound to a snapshot policy") + } + // 若磁盘所属主机已绑定主机快照策略,则磁盘不能再绑定快照策略 + if guest := disk.GetGuest(); guest != nil { + guestCnt, err := SnapshotPolicyResourceManager.GetBindingCount(guest.Id, api.SNAPSHOT_POLICY_TYPE_SERVER) + if err != nil { + return nil, errors.Wrap(err, "GetBindingCount for guest") + } + if guestCnt > 0 { + return nil, httperrors.NewConflictError("guest already has server snapshot policy, disk cannot bind snapshot policy") + } + } spObj, err := validators.ValidateModel(ctx, userCred, SnapshotPolicyManager, &input.SnapshotpolicyId) if err != nil { return nil, err @@ -3190,6 +3228,52 @@ func (disk *SDisk) PerformBindSnapshotpolicy( return nil, sp.StartBindDisksTask(ctx, userCred, []string{disk.Id}) } +// 设置磁盘快照策略 +// 可覆盖当前磁盘绑定的快照策略,若磁盘所属主机已绑定主机快照策略,则自动解除主机快照策略 +func (disk *SDisk) PerformSetSnapshotpolicy( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + input *api.DiskSnapshotpolicyInput, +) (jsonutils.JSONObject, error) { + spObj, err := validators.ValidateModel(ctx, userCred, SnapshotPolicyManager, &input.SnapshotpolicyId) + if err != nil { + return nil, err + } + sp := spObj.(*SSnapshotPolicy) + if sp.Type != api.SNAPSHOT_POLICY_TYPE_DISK { + return nil, httperrors.NewBadRequestError("The snapshot policy %s is not a disk snapshot policy", sp.Name) + } + if len(sp.ManagerId) > 0 { + storage, err := disk.GetStorage() + if err != nil { + return nil, errors.Wrapf(err, "GetStorage") + } + if storage.ManagerId != sp.ManagerId { + return nil, httperrors.NewConflictError("The snapshot policy %s and disk account are different", sp.Name) + } + zone, err := storage.GetZone() + if err != nil { + return nil, errors.Wrapf(err, "GetZone") + } + if sp.CloudregionId != zone.CloudregionId { + return nil, httperrors.NewConflictError("The snapshot policy %s and the disk are in different region", sp.Name) + } + } + // 先解除当前绑定再绑定新策略 + if err := SnapshotPolicyResourceManager.RemoveByResource(disk.Id, api.SNAPSHOT_POLICY_TYPE_DISK); err != nil { + return nil, errors.Wrap(err, "RemoveByResource") + } + // 若磁盘所属主机已绑定主机快照策略,则磁盘不能再绑定快照策略 + if guest := disk.GetGuest(); guest != nil { + if err := SnapshotPolicyResourceManager.RemoveByResource(guest.Id, api.SNAPSHOT_POLICY_TYPE_SERVER); err != nil { + return nil, errors.Wrap(err, "RemoveByResource") + } + } + return nil, sp.StartBindDisksTask(ctx, userCred, []string{disk.Id}) +} + +// 解绑自动快照策略 func (disk *SDisk) PerformUnbindSnapshotpolicy( ctx context.Context, userCred mcclient.TokenCredential, diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index 6ec9aca440..d4d743a3a4 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -5529,6 +5529,96 @@ func (self *SGuest) PerformUnbindGroups(ctx context.Context, userCred mcclient.T return nil, nil } +// 绑定主机快照策略 +// 主机只能绑定一个快照策略,已绑定时报错 +// 若主机下任意磁盘已绑定快照策略则报错 +func (self *SGuest) PerformBindSnapshotpolicy(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, input *api.ServerSnapshotpolicyInput) (jsonutils.JSONObject, error) { + if len(input.SnapshotpolicyId) == 0 { + return nil, httperrors.NewMissingParameterError("snapshotpolicy_id") + } + spObj, err := validators.ValidateModel(ctx, userCred, SnapshotPolicyManager, &input.SnapshotpolicyId) + if err != nil { + return nil, err + } + sp := spObj.(*SSnapshotPolicy) + if sp.Type != api.SNAPSHOT_POLICY_TYPE_SERVER { + return nil, httperrors.NewBadRequestError("The snapshot policy %s is not a server snapshot policy", sp.Name) + } + // 主机只能绑定一个快照策略 + cnt, err := SnapshotPolicyResourceManager.GetBindingCount(self.Id, api.SNAPSHOT_POLICY_TYPE_SERVER) + if err != nil { + return nil, errors.Wrap(err, "GetBindingCount") + } + if cnt > 0 { + return nil, httperrors.NewConflictError("guest already bound to a snapshot policy") + } + // 若主机下任意磁盘已绑定快照策略,则主机不能再绑定主机快照策略 + disks, err := self.GetDisks() + if err != nil { + return nil, errors.Wrap(err, "GetDisks") + } + for _, d := range disks { + diskCnt, err := SnapshotPolicyResourceManager.GetBindingCount(d.Id, api.SNAPSHOT_POLICY_TYPE_DISK) + if err != nil { + return nil, errors.Wrap(err, "GetBindingCount for disk") + } + if diskCnt > 0 { + return nil, httperrors.NewConflictError("guest has disk %s bound to snapshot policy, guest cannot bind server snapshot policy", d.Name) + } + } + sr := &SSnapshotPolicyResource{} + sr.SetModelManager(SnapshotPolicyResourceManager, sr) + sr.SnapshotpolicyId = sp.Id + sr.ResourceId = self.Id + sr.ResourceType = api.SNAPSHOT_POLICY_TYPE_SERVER + if err := SnapshotPolicyResourceManager.TableSpec().Insert(ctx, sr); err != nil { + return nil, errors.Wrap(err, "Insert") + } + logclient.AddActionLogWithContext(ctx, self, logclient.ACT_BIND, input, userCred, true) + return nil, nil +} + +// 设置主机快照策略 +// 可覆盖当前主机绑定的快照策略,若主机下任意磁盘已绑定快照策略,则自动解除磁盘快照策略 +func (self *SGuest) PerformSetSnapshotpolicy(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, input *api.ServerSnapshotpolicyInput) (jsonutils.JSONObject, error) { + if len(input.SnapshotpolicyId) == 0 { + return nil, httperrors.NewMissingParameterError("snapshotpolicy_id") + } + spObj, err := validators.ValidateModel(ctx, userCred, SnapshotPolicyManager, &input.SnapshotpolicyId) + if err != nil { + return nil, err + } + sp := spObj.(*SSnapshotPolicy) + if sp.Type != api.SNAPSHOT_POLICY_TYPE_SERVER { + return nil, httperrors.NewBadRequestError("The snapshot policy %s is not a server snapshot policy", sp.Name) + } + if err := SnapshotPolicyResourceManager.RemoveByResource(self.Id, api.SNAPSHOT_POLICY_TYPE_SERVER); err != nil { + return nil, errors.Wrap(err, "RemoveByResource") + } + // 若主机下任意磁盘已绑定快照策略,则主机不能再绑定主机快照策略 + disks, err := self.GetDisks() + if err != nil { + return nil, errors.Wrap(err, "GetDisks") + } + for _, d := range disks { + if err := SnapshotPolicyResourceManager.RemoveByResource(d.Id, api.SNAPSHOT_POLICY_TYPE_DISK); err != nil { + return nil, errors.Wrap(err, "RemoveByResource") + } + } + sr := &SSnapshotPolicyResource{} + sr.SetModelManager(SnapshotPolicyResourceManager, sr) + sr.SnapshotpolicyId = sp.Id + sr.ResourceId = self.Id + sr.ResourceType = api.SNAPSHOT_POLICY_TYPE_SERVER + if err := SnapshotPolicyResourceManager.TableSpec().Insert(ctx, sr); err != nil { + return nil, errors.Wrap(err, "Insert") + } + logclient.AddActionLogWithContext(ctx, self, logclient.ACT_UPDATE, input, userCred, true) + return nil, nil +} + func (self *SGuest) checkGroups(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (sets.String, error) { diff --git a/pkg/compute/models/guest_queries.go b/pkg/compute/models/guest_queries.go index 1d1388e294..468428d6f2 100644 --- a/pkg/compute/models/guest_queries.go +++ b/pkg/compute/models/guest_queries.go @@ -90,6 +90,15 @@ func (manager *SGuestManager) FetchCustomizeColumns( } } } + if len(fields) == 0 || fields.Contains("snapshotpolicy") { + counts := fetchGuestSnapshotpolicyInfo(guestIds) + for i := range rows { + rows[i].SnapshotpolicyCount = counts[guestIds[i]] + for j := range rows[i].DisksInfo { + rows[i].DisksSnapshotpolicyCount += counts[rows[i].DisksInfo[j].Id] + } + } + } /*if len(fields) == 0 || fields.Contains("ips") { gips := fetchGuestIPs(guestIds, tristate.False) if gips != nil { @@ -440,6 +449,34 @@ func fetchGuestDisksInfo(guestIds []string) map[string][]GuestDiskInfo { return ret } +func fetchGuestSnapshotpolicyInfo(guestIds []string) map[string]int { + ret := map[string]int{} + disks := GuestdiskManager.Query("disk_id").In("guest_id", guestIds).SubQuery() + spq := SnapshotPolicyResourceManager.Query() + spq = spq.Filter(sqlchemy.OR( + sqlchemy.In(spq.Field("resource_id"), guestIds), + sqlchemy.In(spq.Field("resource_id"), disks), + )) + sq := spq.SubQuery() + q := sq.Query( + sq.Field("resource_id"), + sqlchemy.COUNT("count", sq.Field("snapshotpolicy_id")), + ).GroupBy(sq.Field("resource_id")) + counts := []struct { + ResourceId string + Count int + }{} + err := q.All(&counts) + if err != nil { + return nil + } + for _, count := range counts { + ret[count.ResourceId] = count.Count + } + + return ret +} + func (guest *SGuest) GetDisksSize() int { return guest.getDiskSize() } diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 1e1950da48..689202e76b 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -738,6 +738,16 @@ func (manager *SGuestManager) ListItemFilter( q = q.NotIn("id", spjsq) } } + if query.BindingDisksSnapshotpolicy != nil { + guestDisks := GuestdiskManager.Query("guest_id") + sq := SnapshotPolicyResourceManager.Query("resource_id").Equals("resource_type", api.SNAPSHOT_POLICY_TYPE_DISK).SubQuery() + gdsq := guestDisks.Join(sq, sqlchemy.Equals(guestDisks.Field("disk_id"), sq.Field("resource_id"))).SubQuery() + if *query.BindingDisksSnapshotpolicy { + q = q.In("id", gdsq) + } else { + q = q.NotIn("id", gdsq) + } + } return q, nil } diff --git a/pkg/compute/models/snapshot_policy_resources.go b/pkg/compute/models/snapshot_policy_resources.go index 941f4ee1b5..998b6c44bb 100644 --- a/pkg/compute/models/snapshot_policy_resources.go +++ b/pkg/compute/models/snapshot_policy_resources.go @@ -91,3 +91,8 @@ func (man *SSnapshotPolicyResourceManager) RemoveBySnapshotpolicy(id string) err ) return err } + +// GetBindingCount returns the number of snapshot policies bound to the given resource. +func (man *SSnapshotPolicyResourceManager) GetBindingCount(resourceId, resourceType string) (int, error) { + return man.Query().Equals("resource_id", resourceId).Equals("resource_type", resourceType).CountWithError() +} diff --git a/pkg/compute/models/snapshotpolicy.go b/pkg/compute/models/snapshotpolicy.go index bae7253374..21636c0414 100644 --- a/pkg/compute/models/snapshotpolicy.go +++ b/pkg/compute/models/snapshotpolicy.go @@ -510,6 +510,12 @@ func (sp *SSnapshotPolicy) PerformBindDisks( return nil, sp.StartBindDisksTask(ctx, userCred, diskIds) } +// 绑定资源 +// 目前仅支持绑定主机和磁盘 +// 磁盘只能绑定一个快照策略,已绑定时报错 +// 若磁盘所属主机已绑定主机快照策略,则磁盘不能再绑定快照策略 +// 主机只能绑定一个快照策略,已绑定时报错 +// 若主机下任意磁盘已绑定快照策略,则主机不能再绑定主机快照策略 func (sp *SSnapshotPolicy) PerformBindResources( ctx context.Context, userCred mcclient.TokenCredential, @@ -522,15 +528,63 @@ func (sp *SSnapshotPolicy) PerformBindResources( for i := range input.Resources { switch input.Resources[i].Type { case api.SNAPSHOT_POLICY_TYPE_DISK: - _, err := validators.ValidateModel(ctx, userCred, DiskManager, &input.Resources[i].Id) + if sp.Type != api.SNAPSHOT_POLICY_TYPE_DISK { + return nil, httperrors.NewBadRequestError("The snapshot policy %s is not a disk snapshot policy", sp.Name) + } + diskObj, err := validators.ValidateModel(ctx, userCred, DiskManager, &input.Resources[i].Id) if err != nil { return nil, err } + disk := diskObj.(*SDisk) + // 磁盘只能绑定一个快照策略 + cnt, err := SnapshotPolicyResourceManager.GetBindingCount(disk.Id, api.SNAPSHOT_POLICY_TYPE_DISK) + if err != nil { + return nil, errors.Wrap(err, "GetBindingCount") + } + if cnt > 0 { + return nil, httperrors.NewConflictError("disk %s already bound to a snapshot policy", disk.Name) + } + // 若磁盘所属主机已绑定主机快照策略,则磁盘不能再绑定 + if guest := disk.GetGuest(); guest != nil { + guestCnt, err := SnapshotPolicyResourceManager.GetBindingCount(guest.Id, api.SNAPSHOT_POLICY_TYPE_SERVER) + if err != nil { + return nil, errors.Wrap(err, "GetBindingCount for guest") + } + if guestCnt > 0 { + return nil, httperrors.NewConflictError("guest %s already has server snapshot policy, disk cannot bind snapshot policy", guest.Name) + } + } case api.SNAPSHOT_POLICY_TYPE_SERVER: - _, err := validators.ValidateModel(ctx, userCred, GuestManager, &input.Resources[i].Id) + if sp.Type != api.SNAPSHOT_POLICY_TYPE_SERVER { + return nil, httperrors.NewBadRequestError("The snapshot policy %s is not a server snapshot policy", sp.Name) + } + guestObj, err := validators.ValidateModel(ctx, userCred, GuestManager, &input.Resources[i].Id) if err != nil { return nil, err } + guest := guestObj.(*SGuest) + // 主机只能绑定一个快照策略 + cnt, err := SnapshotPolicyResourceManager.GetBindingCount(guest.Id, api.SNAPSHOT_POLICY_TYPE_SERVER) + if err != nil { + return nil, errors.Wrap(err, "GetBindingCount") + } + if cnt > 0 { + return nil, httperrors.NewConflictError("guest %s already bound to a snapshot policy", guest.Name) + } + // 若主机下任意磁盘已绑定快照策略,则主机不能再绑定主机快照策略 + disks, err := guest.GetDisks() + if err != nil { + return nil, errors.Wrap(err, "guest.GetDisks") + } + for _, d := range disks { + diskCnt, err := SnapshotPolicyResourceManager.GetBindingCount(d.Id, api.SNAPSHOT_POLICY_TYPE_DISK) + if err != nil { + return nil, errors.Wrap(err, "GetBindingCount for disk") + } + if diskCnt > 0 { + return nil, httperrors.NewConflictError("guest %s has disk %s bound to snapshot policy, guest cannot bind server snapshot policy", guest.Name, d.Name) + } + } default: return nil, httperrors.NewBadRequestError("Invalid resource type: %s", input.Resources[i].Type) } diff --git a/pkg/mcclient/options/compute/disks.go b/pkg/mcclient/options/compute/disks.go index 425f9c7638..ff3e28e0df 100644 --- a/pkg/mcclient/options/compute/disks.go +++ b/pkg/mcclient/options/compute/disks.go @@ -78,30 +78,34 @@ func (o DiskCreateOptions) Params() (*api.DiskCreateInput, error) { return params, nil } -type DiskMigrateOptions struct { - ID string `help:"ID of the server" json:"-"` - - TargetStorageId string `help:"Disk migrate target storage id or name" json:"target_storage_id"` +type DiskIdOptions struct { + ID string `help:"ID or Name of disk"` } -func (o *DiskMigrateOptions) GetId() string { +func (o *DiskIdOptions) GetId() string { return o.ID } +func (o *DiskIdOptions) Params() (jsonutils.JSONObject, error) { + return nil, nil +} + +type DiskMigrateOptions struct { + DiskIdOptions + + TargetStorageId string `help:"Disk migrate target storage id or name" json:"target_storage_id"` +} + func (o *DiskMigrateOptions) Params() (jsonutils.JSONObject, error) { return options.StructToParams(o) } type DiskChangeStorageTypeOptions struct { - ID string `help:"ID of the server" json:"-"` + DiskIdOptions StorageType string `help:"Disk migrate target storage type" json:"storage_type"` } -func (o *DiskChangeStorageTypeOptions) GetId() string { - return o.ID -} - func (o *DiskChangeStorageTypeOptions) Params() (jsonutils.JSONObject, error) { return options.StructToParams(o) } @@ -124,7 +128,9 @@ type DiskListOptions struct { SnapshotpolicyId string `help:"snapshotpolicy id"` - StorageHostId string `help:"filter disk by host"` + StorageHostId string `help:"filter disk by host"` + BindingServerSnapshotpolicy *bool `help:"filter disk by binding server snapshotpolicy" negative:"no-binding-server-snapshotpolicy"` + BindingSnapshotpolicy *bool `help:"filter disk by binding snapshotpolicy" negative:"no-binding-snapshotpolicy"` } func (opts *DiskListOptions) Params() (jsonutils.JSONObject, error) { @@ -143,14 +149,10 @@ func (opts *DiskListOptions) Params() (jsonutils.JSONObject, error) { } type DiskChangeBillingTypeOptions struct { - ID string + DiskIdOptions BillingType string `choices:"prepaid|postpaid"` } -func (o *DiskChangeBillingTypeOptions) GetId() string { - return o.ID -} - func (o *DiskChangeBillingTypeOptions) Params() (jsonutils.JSONObject, error) { return jsonutils.Marshal(map[string]string{"billing_type": o.BillingType}), nil } diff --git a/pkg/mcclient/options/compute/servers.go b/pkg/mcclient/options/compute/servers.go index 336d29f36a..2d574c6616 100644 --- a/pkg/mcclient/options/compute/servers.go +++ b/pkg/mcclient/options/compute/servers.go @@ -79,8 +79,10 @@ type ServerListOptions struct { WithUserMeta *bool `help:"filter by user metadata" negative:"without_user_meta"` - WithHost *bool `help:"filter guest with host or not" negative:"without_host"` - SnapshotpolicyId string `help:"filter guest with snapshotpolicy or not" json:"snapshotpolicy_id"` + WithHost *bool `help:"filter guest with host or not" negative:"without_host"` + SnapshotpolicyId string `help:"filter guest with snapshotpolicy or not" json:"snapshotpolicy_id"` + BindingDisksSnapshotpolicy *bool `help:"filter guest with disks binding snapshotpolicy or not" negative:"no-binding-disks-snapshotpolicy" json:"binding_disks_snapshotpolicy"` + BindingSnapshotpolicy *bool `help:"filter guest with binding snapshotpolicy or not" negative:"no-binding-snapshotpolicy" json:"binding_snapshotpolicy"` } func (o *ServerListOptions) Params() (jsonutils.JSONObject, error) {