From adbefb74bffa20cc42c103476dc6456646e456a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=88=E8=BD=A9?= Date: Thu, 4 Jun 2026 18:51:48 +0800 Subject: [PATCH] fix(region): prefer zones (#24966) --- pkg/apis/compute/api.go | 19 ++- pkg/apis/compute/disk.go | 10 +- pkg/apis/compute/serversku.go | 27 ++++ pkg/compute/models/disks.go | 11 +- pkg/compute/models/guests.go | 21 ++- pkg/compute/models/helper.go | 33 +++++ pkg/compute/models/server_skus.go | 126 ++++++++++++++++++ pkg/mcclient/options/compute/disks.go | 14 +- pkg/mcclient/options/compute/servers.go | 12 +- .../predicates/guest/image_predicate.go | 2 +- .../algorithm/predicates/sku_predicate.go | 4 +- .../predicates/zone_schedtag_predicate.go | 6 +- .../data_manager/candidate_manager.go | 13 +- pkg/scheduler/manager/scheduler.go | 2 +- 14 files changed, 270 insertions(+), 30 deletions(-) diff --git a/pkg/apis/compute/api.go b/pkg/apis/compute/api.go index c8560b36a5..926d59c443 100644 --- a/pkg/apis/compute/api.go +++ b/pkg/apis/compute/api.go @@ -372,7 +372,10 @@ type ServerConfigs struct { PreferRegion string `json:"prefer_region_id"` // 调度到指定可用区,优先级低于prefer_host_id - PreferZone string `json:"prefer_zone_id"` + PreferZone string `json:"prefer_zone_id" yunion-deprecated-by:"prefer_zones"` + + // 调度到指定可用区列表,优先级低于prefer_host_id + PreferZones []string `json:"prefer_zones"` // 调度使用指定二层网络, 优先级低于prefer_host_id PreferWire string `json:"prefer_wire_id"` @@ -480,6 +483,20 @@ func NewServerConfigs() *ServerConfigs { } } +func (c *ServerConfigs) GetPreferZones() []string { + if len(c.PreferZones) > 0 { + return c.PreferZones + } + if c.PreferZone != "" { + return []string{c.PreferZone} + } + return nil +} + +func (c *ServerConfigs) HasPreferZone() bool { + return len(c.GetPreferZones()) > 0 +} + type DeployConfig struct { Action string `json:"action"` Path string `json:"path"` diff --git a/pkg/apis/compute/disk.go b/pkg/apis/compute/disk.go index dbe757fa86..ce4e9c91ad 100644 --- a/pkg/apis/compute/disk.go +++ b/pkg/apis/compute/disk.go @@ -48,9 +48,13 @@ type DiskCreateInput struct { // required: false PreferRegion string `json:"prefer_region_id"` - // 此参数仅适用于未指定storage时进行调度到指定可用区区创建磁盘 + // 此参数仅适用于未指定storage时进行调度到指定可用区创建磁盘 // required: false - PreferZone string `json:"prefer_zone_id"` + PreferZone string `json:"prefer_zone_id" yunion-deprecated-by:"prefer_zones"` + + // 此参数仅适用于未指定storage时进行调度到指定可用区列表创建磁盘 + // required: false + PreferZones []string `json:"prefer_zones"` // swagger:ignore PreferWire string `json:"prefer_wire_id"` @@ -72,6 +76,7 @@ func (req *DiskCreateInput) ToServerCreateInput() *ServerCreateInput { PreferManager: req.PreferManager, PreferRegion: req.PreferRegion, PreferZone: req.PreferZone, + PreferZones: req.PreferZones, PreferWire: req.PreferWire, PreferHost: req.PreferHost, Hypervisor: req.Hypervisor, @@ -92,6 +97,7 @@ func (req *ServerCreateInput) ToDiskCreateInput() *DiskCreateInput { PreferRegion: req.PreferRegion, PreferHost: req.PreferHost, PreferZone: req.PreferZone, + PreferZones: req.PreferZones, PreferWire: req.PreferWire, Hypervisor: req.Hypervisor, } diff --git a/pkg/apis/compute/serversku.go b/pkg/apis/compute/serversku.go index 304c312c84..644302cb72 100644 --- a/pkg/apis/compute/serversku.go +++ b/pkg/apis/compute/serversku.go @@ -142,6 +142,33 @@ type ServerSkuDetails struct { // 绑定云主机数量 TotalGuestCount int `json:"total_guest_count"` + + // 同名套餐在各区域/可用区的计费可用性 + RegionalAvailability []ServerSkuRegionalAvailability `json:"regional_availability"` +} + +type ServerSkuRegionalAvailability struct { + // 区域Id + CloudregionId string `json:"cloudregion_id"` + // 区域名称 + Cloudregion string `json:"cloudregion"` + // 区域级套餐后付费(按量)状态, zone_id 为空时有效 + PostpaidStatus string `json:"postpaid_status,omitempty"` + // 区域级套餐预付费(包年包月)状态, zone_id 为空时有效 + PrepaidStatus string `json:"prepaid_status,omitempty"` + // 可用区级套餐可用性 + Zones []ServerSkuZoneAvailability `json:"zones,omitempty"` +} + +type ServerSkuZoneAvailability struct { + // 可用区Id + ZoneId string `json:"zone_id"` + // 可用区名称 + Zone string `json:"zone"` + // 后付费(按量)状态 available|soldout + PostpaidStatus string `json:"postpaid_status"` + // 预付费(包年包月)状态 available|soldout + PrepaidStatus string `json:"prepaid_status"` } type ServerSkuUpdateInput struct { diff --git a/pkg/compute/models/disks.go b/pkg/compute/models/disks.go index c3079a53f4..f5c191c1bd 100644 --- a/pkg/compute/models/disks.go +++ b/pkg/compute/models/disks.go @@ -565,7 +565,16 @@ func diskCreateInput2ComputeQuotaKeys(input api.DiskCreateInput, ownerId mcclien keys.ZoneId = wire.ZoneId } } - if len(input.PreferZone) > 0 { + if len(input.PreferZones) > 0 { + zoneObj, err := ZoneManager.FetchById(input.PreferZones[0]) + if err != nil { + return keys, err + } + zone := zoneObj.(*SZone) + input.PreferRegion = zone.CloudregionId + keys.ZoneId = zone.Id + keys.RegionId = zone.CloudregionId + } else if len(input.PreferZone) > 0 { zoneObj, err := ZoneManager.FetchById(input.PreferZone) if err != nil { return keys, err diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 6869f20360..41d9475317 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -1569,7 +1569,16 @@ func serverCreateInput2ComputeQuotaKeys(input api.ServerCreateInput, ownerId mcc keys.ZoneId = wire.ZoneId } } - if len(input.PreferZone) > 0 { + if len(input.PreferZones) > 0 { + zoneObj, err := ZoneManager.FetchById(input.PreferZones[0]) + if err != nil { + return keys, err + } + zone := zoneObj.(*SZone) + input.PreferRegion = zone.CloudregionId + keys.ZoneId = zone.Id + keys.RegionId = zone.CloudregionId + } else if len(input.PreferZone) > 0 { zoneObj, err := ZoneManager.FetchById(input.PreferZone) if err != nil { return keys, err @@ -1768,7 +1777,15 @@ func (manager *SGuestManager) validateCreateData( return nil, errors.Wrap(err, "checkGuestImage") } - if len(input.PreferZone) > 0 && len(input.Provider) == 0 { + preferZones := input.GetPreferZones() + if len(preferZones) > 0 && len(input.Provider) == 0 { + zoneObj, err := ZoneManager.FetchById(preferZones[0]) + if err != nil { + return nil, errors.Wrapf(err, "zone fetch by id %s", preferZones[0]) + } + zone := zoneObj.(*SZone) + input.PreferRegion = zone.CloudregionId + } else if len(input.PreferZone) > 0 && len(input.Provider) == 0 { zoneObj, err := ZoneManager.FetchById(input.PreferZone) if err != nil { return nil, errors.Wrapf(err, "zone fetch by id %s", input.PreferZone) diff --git a/pkg/compute/models/helper.go b/pkg/compute/models/helper.go index dc387ab376..239bce70bc 100644 --- a/pkg/compute/models/helper.go +++ b/pkg/compute/models/helper.go @@ -29,6 +29,35 @@ import ( "yunion.io/x/onecloud/pkg/mcclient" ) +func validatePreferZones(ctx context.Context, userCred mcclient.IIdentityProvider, input *api.ServerConfigs) error { + if len(input.PreferZones) == 0 { + return nil + } + zoneIds := make([]string, 0, len(input.PreferZones)) + var regionId string + for _, zoneStr := range input.PreferZones { + zoneObj, err := ZoneManager.FetchByIdOrName(ctx, userCred, zoneStr) + if err != nil { + if err == sql.ErrNoRows { + return httperrors.NewResourceNotFoundError("Zone %s not found", zoneStr) + } + return httperrors.NewGeneralError(err) + } + zone := zoneObj.(*SZone) + if regionId == "" { + regionId = zone.CloudregionId + } else if regionId != zone.CloudregionId { + return httperrors.NewInputParameterError("All prefer zones must be in the same region") + } + zoneIds = append(zoneIds, zone.Id) + } + input.PreferZones = zoneIds + if input.PreferRegion == "" { + input.PreferRegion = regionId + } + return nil +} + func RunBatchCreateTask( ctx context.Context, items []db.IModel, @@ -149,6 +178,10 @@ func ValidateScheduleCreateData(ctx context.Context, userCred mcclient.TokenCred input.PreferZone = zone.Id region, _ := zone.GetRegion() input.PreferRegion = region.Id + } else if len(input.PreferZones) > 0 { + if err := validatePreferZones(ctx, userCred, input.ServerConfigs); err != nil { + return nil, err + } } else if input.PreferZone != "" { zoneStr := input.PreferZone zoneObj, err := ZoneManager.FetchByIdOrName(ctx, userCred, zoneStr) diff --git a/pkg/compute/models/server_skus.go b/pkg/compute/models/server_skus.go index 8cb97d3a13..8475915597 100644 --- a/pkg/compute/models/server_skus.go +++ b/pkg/compute/models/server_skus.go @@ -187,6 +187,121 @@ func (self SServerSku) GetGlobalId() string { return self.ExternalId } +func skuAvailabilityKey(name, provider string) string { + return name + "\x00" + provider +} + +func (manager *SServerSkuManager) fetchServerSkuAvailabilityMap(names []string) (map[string][]api.ServerSkuRegionalAvailability, error) { + ret := map[string][]api.ServerSkuRegionalAvailability{} + if len(names) == 0 { + return ret, nil + } + + skus := []struct { + Name string + Provider string + CloudregionId string + ZoneId string + PostpaidStatus string + PrepaidStatus string + }{} + q := manager.Query("name", "provider", "cloudregion_id", "zone_id", "postpaid_status", "prepaid_status"). + IsTrue("enabled").In("name", names) + err := q.All(&skus) + if err != nil { + return nil, errors.Wrap(err, "query availability skus") + } + + type regionAvail struct { + cloudregionId string + postpaidStatus string + prepaidStatus string + hasRegionLevel bool + zones map[string]api.ServerSkuZoneAvailability + } + + tmp := map[string]map[string]*regionAvail{} + regionIdSet := map[string]bool{} + zoneIdSet := map[string]bool{} + + for i := range skus { + sku := &skus[i] + key := skuAvailabilityKey(sku.Name, sku.Provider) + if _, ok := tmp[key]; !ok { + tmp[key] = map[string]*regionAvail{} + } + regionMap := tmp[key] + if _, ok := regionMap[sku.CloudregionId]; !ok { + regionMap[sku.CloudregionId] = ®ionAvail{ + cloudregionId: sku.CloudregionId, + zones: map[string]api.ServerSkuZoneAvailability{}, + } + regionIdSet[sku.CloudregionId] = true + } + ra := regionMap[sku.CloudregionId] + if len(sku.ZoneId) == 0 { + ra.hasRegionLevel = true + ra.postpaidStatus = sku.PostpaidStatus + ra.prepaidStatus = sku.PrepaidStatus + } else { + ra.zones[sku.ZoneId] = api.ServerSkuZoneAvailability{ + ZoneId: sku.ZoneId, + PostpaidStatus: sku.PostpaidStatus, + PrepaidStatus: sku.PrepaidStatus, + } + zoneIdSet[sku.ZoneId] = true + } + } + + regionIds := make([]string, 0, len(regionIdSet)) + for id := range regionIdSet { + regionIds = append(regionIds, id) + } + zoneIds := make([]string, 0, len(zoneIdSet)) + for id := range zoneIdSet { + zoneIds = append(zoneIds, id) + } + + regionNames, err := db.FetchIdNameMap2(CloudregionManager, regionIds) + if err != nil { + return nil, errors.Wrap(err, "FetchIdNameMap2 cloudregion") + } + zoneNames, err := db.FetchIdNameMap2(ZoneManager, zoneIds) + if err != nil { + return nil, errors.Wrap(err, "FetchIdNameMap2 zone") + } + + for key, regionMap := range tmp { + avails := make([]api.ServerSkuRegionalAvailability, 0, len(regionMap)) + for _, ra := range regionMap { + item := api.ServerSkuRegionalAvailability{ + CloudregionId: ra.cloudregionId, + Cloudregion: regionNames[ra.cloudregionId], + } + if ra.hasRegionLevel { + item.PostpaidStatus = ra.postpaidStatus + item.PrepaidStatus = ra.prepaidStatus + } + if len(ra.zones) > 0 { + item.Zones = make([]api.ServerSkuZoneAvailability, 0, len(ra.zones)) + for zoneId, za := range ra.zones { + za.Zone = zoneNames[zoneId] + item.Zones = append(item.Zones, za) + } + sort.Slice(item.Zones, func(i, j int) bool { + return item.Zones[i].Zone < item.Zones[j].Zone + }) + } + avails = append(avails, item) + } + sort.Slice(avails, func(i, j int) bool { + return avails[i].Cloudregion < avails[j].Cloudregion + }) + ret[key] = avails + } + return ret, nil +} + func (manager *SServerSkuManager) FetchCustomizeColumns( ctx context.Context, userCred mcclient.TokenCredential, @@ -263,6 +378,17 @@ func (manager *SServerSkuManager) FetchCustomizeColumns( } } + availMap, err := manager.fetchServerSkuAvailabilityMap(instanceTypes) + if err != nil { + log.Errorf("fetchServerSkuAvailabilityMap error: %v", err) + return rows + } + for i := range rows { + sku := objs[i].(*SServerSku) + key := skuAvailabilityKey(sku.Name, sku.Provider) + rows[i].RegionalAvailability = availMap[key] + } + return rows } diff --git a/pkg/mcclient/options/compute/disks.go b/pkg/mcclient/options/compute/disks.go index f73f1c92be..5f1ae5f4c6 100644 --- a/pkg/mcclient/options/compute/disks.go +++ b/pkg/mcclient/options/compute/disks.go @@ -23,12 +23,13 @@ import ( ) type DiskCreateOptions struct { - Manager string `help:"Preferred manager where virtual server should be created" json:"prefer_manager_id"` - Region string `help:"Preferred region where virtual server should be created" json:"prefer_region_id"` - Zone string `help:"Preferred zone where virtual server should be created" json:"prefer_zone_id"` - Wire string `help:"Preferred wire where virtual server should be created" json:"prefer_wire_id"` - Host string `help:"Preferred host where virtual server should be created" json:"prefer_host_id"` - Count int `help:"Count to create" json:"count"` + Manager string `help:"Preferred manager where virtual server should be created" json:"prefer_manager_id"` + Region string `help:"Preferred region where virtual server should be created" json:"prefer_region_id"` + Zone string `help:"Preferred zone where virtual server should be created" json:"prefer_zone_id"` + Zones []string `help:"Preferred zones where virtual server should be created" json:"prefer_zones"` + Wire string `help:"Preferred wire where virtual server should be created" json:"prefer_wire_id"` + Host string `help:"Preferred host where virtual server should be created" json:"prefer_host_id"` + Count int `help:"Count to create" json:"count"` NAME string `help:"Name of the disk"` DISKDESC string `help:"Image size or size of virtual disk"` @@ -63,6 +64,7 @@ func (o DiskCreateOptions) Params() (*api.DiskCreateInput, error) { PreferManager: o.Manager, PreferRegion: o.Region, PreferZone: o.Zone, + PreferZones: o.Zones, PreferWire: o.Wire, PreferHost: o.Host, DiskConfig: config, diff --git a/pkg/mcclient/options/compute/servers.go b/pkg/mcclient/options/compute/servers.go index e7948c3182..643d7a6437 100644 --- a/pkg/mcclient/options/compute/servers.go +++ b/pkg/mcclient/options/compute/servers.go @@ -249,11 +249,12 @@ func ParseServerDeployInfoList(list []string) ([]*computeapi.DeployConfig, error } type ServerCreateCommonConfig struct { - Manager string `help:"Preferred cloudprovider where virtual server should bd created" json:"prefer_manager"` - Region string `help:"Preferred region where virtual server should be created" json:"prefer_region"` - Zone string `help:"Preferred zone where virtual server should be created" json:"prefer_zone"` - Wire string `help:"Preferred wire where virtual server should be created" json:"prefer_wire"` - Host string `help:"Preferred host where virtual server should be created" json:"prefer_host"` + Manager string `help:"Preferred cloudprovider where virtual server should bd created" json:"prefer_manager"` + Region string `help:"Preferred region where virtual server should be created" json:"prefer_region"` + Zone string `help:"Preferred zone where virtual server should be created" json:"prefer_zone"` + Zones []string `help:"Preferred zones where virtual server should be created" json:"prefer_zones"` + Wire string `help:"Preferred wire where virtual server should be created" json:"prefer_wire"` + Host string `help:"Preferred host where virtual server should be created" json:"prefer_host"` ResourceType string `help:"Resource type" choices:"shared|prepaid|dedicated"` Schedtag []string `help:"Schedule policy, key = aggregate name, value = require|exclude|prefer|avoid" metavar:""` @@ -294,6 +295,7 @@ func (o ServerCreateCommonConfig) Data() (*computeapi.ServerConfigs, error) { PreferManager: o.Manager, PreferRegion: o.Region, PreferZone: o.Zone, + PreferZones: o.Zones, PreferWire: o.Wire, PreferHost: o.Host, ResourceType: o.ResourceType, diff --git a/pkg/scheduler/algorithm/predicates/guest/image_predicate.go b/pkg/scheduler/algorithm/predicates/guest/image_predicate.go index 65483a625f..99951f3343 100644 --- a/pkg/scheduler/algorithm/predicates/guest/image_predicate.go +++ b/pkg/scheduler/algorithm/predicates/guest/image_predicate.go @@ -52,7 +52,7 @@ func (f *ImagePredicate) PreExecute(ctx context.Context, u *core.Unit, cs []core return false, nil } imageId := disks[0].ImageId - if len(imageId) == 0 || u.SchedData().PreferZone != "" { + if len(imageId) == 0 || u.SchedData().HasPreferZone() { return false, nil } if !utils.IsInStringArray(u.SchedData().Provider, compute.PUBLIC_CLOUD_PROVIDERS) && !utils.IsInStringArray(u.SchedData().Provider, compute.PRIVATE_CLOUD_PROVIDERS) { diff --git a/pkg/scheduler/algorithm/predicates/sku_predicate.go b/pkg/scheduler/algorithm/predicates/sku_predicate.go index e1c7fef10e..a963d93e3b 100644 --- a/pkg/scheduler/algorithm/predicates/sku_predicate.go +++ b/pkg/scheduler/algorithm/predicates/sku_predicate.go @@ -58,9 +58,9 @@ func (p *InstanceTypePredicate) Execute(ctx context.Context, u *core.Unit, c cor instanceType := d.InstanceType reqRegion := d.PreferRegion - reqZone := d.PreferZone + reqZones := d.GetPreferZones() - if reqRegion != "" && reqZone == "" { + if reqRegion != "" && len(reqZones) == 0 { skus := skuman.GetByRegion(instanceType, regionId) if len(skus) == 0 { h.Exclude(fmt.Sprintf("Not found server sku %s at region %s", instanceType, regionName)) diff --git a/pkg/scheduler/algorithm/predicates/zone_schedtag_predicate.go b/pkg/scheduler/algorithm/predicates/zone_schedtag_predicate.go index 8e581c2e06..47539eac35 100644 --- a/pkg/scheduler/algorithm/predicates/zone_schedtag_predicate.go +++ b/pkg/scheduler/algorithm/predicates/zone_schedtag_predicate.go @@ -43,7 +43,7 @@ func (p *ZoneSchedtagPredicate) Clone() core.FitPredicate { type zoneSchedtagInputW struct { schedData *api.SchedInfo - zone string + zones []string schedtags []*computeapi.SchedtagConfig } @@ -57,7 +57,7 @@ func (p *ZoneSchedtagPredicate) GetInputs(u *core.Unit) []ISchedtagCustomer { return []ISchedtagCustomer{ &zoneSchedtagInputW{ schedData: data, - zone: data.PreferZone, + zones: data.GetPreferZones(), schedtags: schedtags, }, } @@ -76,7 +76,7 @@ func (w *zoneSchedtagInputW) GetDynamicConditionInput() *jsonutils.JSONDict { } func (w *zoneSchedtagInputW) IsSpecifyResource() bool { - return w.zone != "" + return len(w.zones) > 0 } func (w *zoneSchedtagInputW) GetSchedtags() []*computeapi.SchedtagConfig { diff --git a/pkg/scheduler/data_manager/candidate_manager.go b/pkg/scheduler/data_manager/candidate_manager.go index 8f91fcbfdd..5c505f28b8 100644 --- a/pkg/scheduler/data_manager/candidate_manager.go +++ b/pkg/scheduler/data_manager/candidate_manager.go @@ -33,6 +33,7 @@ type CandidateGetArgs struct { ResType string RegionID string ZoneID string + ZoneIDs []string ManagerID string HostTypes []string } @@ -208,12 +209,12 @@ func (cm *CandidateManager) GetCandidates(args CandidateGetArgs) ([]core.Candida result := []core.Candidater{} - matchZone := func(r core.Candidater, zoneId string) bool { + matchZone := func(r core.Candidater, zoneId string, zoneIds []string) bool { + if len(zoneIds) > 0 { + return utils.IsInStringArray(r.Getter().Zone().GetId(), zoneIds) + } if zoneId != "" { - if r.Getter().Zone().GetId() == zoneId { - return true - } - return false + return r.Getter().Zone().GetId() == zoneId } return true } @@ -258,7 +259,7 @@ func (cm *CandidateManager) GetCandidates(args CandidateGetArgs) ([]core.Candida continue } - if !matchZone(r, args.ZoneID) { + if !matchZone(r, args.ZoneID, args.ZoneIDs) { continue } diff --git a/pkg/scheduler/manager/scheduler.go b/pkg/scheduler/manager/scheduler.go index eedeab058d..4b561a685c 100644 --- a/pkg/scheduler/manager/scheduler.go +++ b/pkg/scheduler/manager/scheduler.go @@ -44,7 +44,7 @@ func candidatesByProvider(provider CandidatesProvider, schedData *api.SchedInfo) } else { args := data_manager.CandidateGetArgs{ ResType: provider.CandidateType(), - ZoneID: schedData.PreferZone, + ZoneIDs: schedData.GetPreferZones(), RegionID: schedData.PreferRegion, ManagerID: schedData.PreferManager, HostTypes: schedData.GetCandidateHostTypes(),