From da709bb94137793811bc03614ed7337c2f8c8934 Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Wed, 29 Apr 2026 01:00:11 +0200 Subject: [PATCH 1/2] ui: derive LB algorithm dropdown from network's Lb provider capability Before this change, both the public LB form (LoadBalancing.vue) and the internal LB form (network.js Internal LB action) hard-coded the algorithm dropdown to ['source', 'roundrobin', 'leastconn']. Every Lb provider got the same three options regardless of what the provider actually supported, so a user could pick "leastconn" against, e.g., the OVN provider, only to get the rule rejected later by validateLBRule. The capability is already declared (NetworkElement.getCapabilities() -> Service.Lb -> Capability.SupportedLBAlgorithms) and surfaced by listNetworks - we just weren't reading it. LoadBalancing.vue (public LB - the create modal and the edit-rule modal): - Replaces the three static s with a v-for over a new `supportedAlgorithms` data field defaulting to the legacy ['roundrobin', 'leastconn', 'source'] (so providers/networks that don't declare the capability behave exactly as before). - Adds fetchLbCapabilities() (called from fetchData) which: * resolves the network id from resource.associatednetworkid or resource.networkid, * looks up the network via listNetworks, * walks service[name=Lb].capability[name=SupportedLbAlgorithms] and splits the CSV into the dropdown list, * snaps newRule.algorithm to the first supported value if the previous default falls outside the new set, * swallows any error so we never break the page on a backend hiccup. listNetworks is the right endpoint here. listNetworkOfferings hard-codes only SupportedLBIsolation/ElasticLb/InlineMode/VmAutoScaling for the Lb service in createNetworkOfferingResponse, while createNetworkResponse walks the live provider's getCapabilities() and exposes everything we declared - including SupportedLbAlgorithms. network.js (internal LB action): - Leaves the static option list in place but flags the spot with a TODO. Driving this declaratively requires AutogenView to support an async or function-valued `mapping[field].options`, which is a framework-level change and out of scope for this UI patch. A follow-up can convert the declarative mapping to a SFC and adopt the same approach as the public LB form. The change is provider-agnostic: it benefits every Lb provider that correctly declares SupportedLBAlgorithms. OVN gets {roundrobin, source}; VirtualRouter keeps {roundrobin, leastconn, source}; Netscaler/F5/etc. will reflect whatever they declare, and providers without the capability fall back to the legacy three. This commit is intentionally self-contained on top of the OVN plugin work so it can be cherry-picked into a dedicated upstream PR. Co-Authored-By: Claude Opus 4.7 --- ui/src/config/section/network.js | 5 +++ ui/src/views/network/LoadBalancing.vue | 54 +++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index 37cdd0c8b98a..bc45f4c8eacf 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -1139,6 +1139,11 @@ export default { args: ['name', 'description', 'sourceipaddress', 'sourceport', 'instanceport', 'algorithm', 'networkid', 'sourceipaddressnetworkid', 'scheme'], mapping: { algorithm: { + // TODO: derive from the selected network's offering capabilities + // (service Lb, SupportedLBAlgorithms) - mirrors what LoadBalancing.vue + // does for public LB. Doing it here requires AutogenView to support + // an async/function-valued `options`, which is a framework change + // outside the scope of this UI patch. options: ['source', 'roundrobin', 'leastconn'] }, scheme: { diff --git a/ui/src/views/network/LoadBalancing.vue b/ui/src/views/network/LoadBalancing.vue index 0b9ed7684a89..fd2bf88d205f 100644 --- a/ui/src/views/network/LoadBalancing.vue +++ b/ui/src/views/network/LoadBalancing.vue @@ -49,9 +49,7 @@ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }" > - {{ $t('label.lb.algorithm.roundrobin') }} - {{ $t('label.lb.algorithm.leastconn') }} - {{ $t('label.lb.algorithm.source') }} + {{ $t('label.lb.algorithm.' + algo) }}
@@ -436,9 +434,7 @@ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }" > - {{ $t('label.lb.algorithm.roundrobin') }} - {{ $t('label.lb.algorithm.leastconn') }} - {{ $t('label.lb.algorithm.source') }} + {{ $t('label.lb.algorithm.' + algo) }}
@@ -872,6 +868,10 @@ export default { cidrlist: '' }, lbProvider: null, + // Populated from the network's offering capabilities (service Lb, + // SupportedLBAlgorithms). Defaults to the union we historically hard-coded so + // legacy networks/offerings without an explicit cap keep working. + supportedAlgorithms: ['roundrobin', 'leastconn', 'source'], addVmModalVisible: false, addVmModalLoading: false, addVmModalNicLoading: false, @@ -1089,6 +1089,48 @@ export default { this.fetchListTiers() this.fetchLBRules() this.fetchZone() + this.fetchLbCapabilities() + }, + /** + * Loads the SupportedLbAlgorithms capability from the network's Lb service. + * The capability lives at network.service[name=Lb].capability[name=SupportedLbAlgorithms] + * with a CSV value like "roundrobin,source". Falls back to the legacy + * static list when the lookup fails so providers that don't declare it + * keep their pre-existing behaviour. + * + * Note: listNetworks is the right endpoint here (not listNetworkOfferings): + * the offering response hard-codes only SupportedLBIsolation/ElasticLb/ + * InlineMode/VmAutoScaling for the Lb service, while the network response + * walks the live provider's getCapabilities() and exposes everything we + * declared. The capability name on the wire is "SupportedLbAlgorithms" + * (note the lowercase 'b' in 'Lb') - that's how Capability.SupportedLBAlgorithms.getName() + * serialises. + */ + fetchLbCapabilities () { + const networkId = this.resource?.associatednetworkid || this.resource?.networkid + if (!networkId) { + return + } + getAPI('listNetworks', { id: networkId, listall: true }).then(json => { + const network = json?.listnetworksresponse?.network?.[0] + const lbService = network?.service?.find(s => s.name === 'Lb') + const algoCap = lbService?.capability?.find(c => c.name === 'SupportedLbAlgorithms') + if (algoCap && algoCap.value) { + const algos = algoCap.value.split(',').map(s => s.trim()).filter(s => s) + if (algos.length > 0) { + this.supportedAlgorithms = algos + // If the default algorithm is not in the supported set, switch to the + // first one so the dropdown shows a valid selection from the start. + if (!algos.includes(this.newRule.algorithm)) { + this.newRule.algorithm = algos[0] + } + } + } + }).catch(error => { + // Swallow - we just keep the default list. Don't pop a notification + // because this is non-fatal background data. + console.warn('Failed to load LB algorithm capabilities; using defaults', error) + }) }, fetchVpc () { if (!this.resource.vpcid) { From d756c1ae87f6356c1896d1bc02d88fbf28b171dc Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Wed, 29 Apr 2026 13:08:16 +0200 Subject: [PATCH 2/2] ui: derive LB algorithm dropdown from capability on every LB form Extends the data-driven LB algorithm dropdown previously added in LoadBalancing.vue (public-IP LB form) to the two remaining places where the capability was still hard-coded as ['source', 'roundrobin', 'leastconn']: - VpcTiersTab.vue: the "Add Internal LB" modal accessible from the VPC details page. The dropdown now follows the capability declared by the selected tier's Lb provider, looked up via listNetworks at the moment the modal opens (handleAddInternalLB -> fetchLbCapabilitiesForNetwork). Falls back to the legacy three when the lookup fails or the provider does not declare the capability, so providers without it behave exactly as before. - AutoScaleLoadBalancing.vue: the "Edit rule" modal on a VPC public AutoScale LB. Same pattern - openEditRuleModal -> fetchLbCapabilitiesForRule reads the capability from the rule's tier network and narrows the dropdown to whatever the provider actually accepts. LoadBalancing.vue: keeps its existing per-network lookup but adds a fallback for the VPC case. When the public IP record exposes only vpcid (the IP has not been associated to a specific tier yet because no rule exists), the helper now picks any Lb-enabled tier of the VPC via listNetworks(vpcid=..., supportedservices=Lb) and reads the capability from that tier. Every tier of the same VPC uses the same Lb provider, so the answer is provider-uniform. The change is provider-agnostic - any Lb provider that correctly declares Service.Lb -> Capability.SupportedLBAlgorithms benefits. The OVN provider exposes {roundrobin, source}, VirtualRouter keeps {roundrobin, leastconn, source}, NetScaler / F5 / etc. will reflect whatever they declare, and providers without the capability fall back to the legacy three. listNetworks remains the right endpoint because the offering response only emits a fixed subset of Lb capabilities while createNetworkResponse walks the live provider's getCapabilities() and surfaces SupportedLbAlgorithms verbatim. Co-Authored-By: Claude Opus 4.7 --- .../views/compute/AutoScaleLoadBalancing.vue | 38 ++++++++++++-- ui/src/views/network/LoadBalancing.vue | 47 ++++++++++++----- ui/src/views/network/VpcTiersTab.vue | 50 ++++++++++++++++--- 3 files changed, 111 insertions(+), 24 deletions(-) diff --git a/ui/src/views/compute/AutoScaleLoadBalancing.vue b/ui/src/views/compute/AutoScaleLoadBalancing.vue index 6c04ce1c2504..deebd93f26ef 100644 --- a/ui/src/views/compute/AutoScaleLoadBalancing.vue +++ b/ui/src/views/compute/AutoScaleLoadBalancing.vue @@ -267,9 +267,7 @@ :filterOption="(input, option) => { return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 }" > - {{ $t('label.lb.algorithm.roundrobin') }} - {{ $t('label.lb.algorithm.leastconn') }} - {{ $t('label.lb.algorithm.source') }} + {{ $t('label.lb.algorithm.' + algo) }}
@@ -338,6 +336,10 @@ export default { algorithm: '', protocol: '' }, + // Default fallback list. Replaced by fetchLbCapabilitiesForRule() with whatever the + // rule's tier network advertises via service.Lb.capability.SupportedLbAlgorithms, + // matching the pattern in LoadBalancing.vue and VpcTiersTab.vue. + supportedAlgorithms: ['roundrobin', 'leastconn', 'source'], vms: [], nics: [], totalCount: 0, @@ -836,6 +838,36 @@ export default { this.editRuleDetails.name = this.selectedRule.name this.editRuleDetails.algorithm = this.selectedRule.algorithm this.editRuleDetails.protocol = this.selectedRule.protocol + this.fetchLbCapabilitiesForRule(this.selectedRule) + }, + /** + * Loads SupportedLbAlgorithms from the LB rule's tier network. Same approach as + * LoadBalancing.vue's fetchLbCapabilities() and VpcTiersTab.vue's + * fetchLbCapabilitiesForNetwork() — capability is read from the live network + * (listNetworks) rather than the offering, since the offering response does not + * carry the per-provider Lb capability map. + */ + fetchLbCapabilitiesForRule (rule) { + const networkId = rule?.networkid + if (!networkId) { + return + } + getAPI('listNetworks', { id: networkId, listall: true }).then(json => { + const network = json?.listnetworksresponse?.network?.[0] + const lbService = network?.service?.find(s => s.name === 'Lb') + const algoCap = lbService?.capability?.find(c => c.name === 'SupportedLbAlgorithms') + if (algoCap && algoCap.value) { + const algos = algoCap.value.split(',').map(s => s.trim()).filter(s => s) + if (algos.length > 0) { + this.supportedAlgorithms = algos + if (!algos.includes(this.editRuleDetails.algorithm)) { + this.editRuleDetails.algorithm = algos[0] + } + } + } + }).catch(error => { + console.warn('Failed to load AutoScale LB algorithm capabilities; using defaults', error) + }) }, handleSubmitEditForm () { if (this.editRuleModalLoading) return diff --git a/ui/src/views/network/LoadBalancing.vue b/ui/src/views/network/LoadBalancing.vue index fd2bf88d205f..95b982d3cd47 100644 --- a/ui/src/views/network/LoadBalancing.vue +++ b/ui/src/views/network/LoadBalancing.vue @@ -1108,23 +1108,29 @@ export default { */ fetchLbCapabilities () { const networkId = this.resource?.associatednetworkid || this.resource?.networkid - if (!networkId) { + if (networkId) { + this.fetchLbCapabilitiesByNetworkId(networkId) return } + // VPC public IP that has not been associated to a specific tier yet (no rule yet). + // Pick any Lb-enabled tier of the VPC and read the capability from there - every tier + // of the same VPC uses the same Lb provider, so the answer is provider-uniform. + if (this.resource?.vpcid) { + getAPI('listNetworks', { vpcid: this.resource.vpcid, supportedservices: 'Lb', listall: true }).then(json => { + const tier = json?.listnetworksresponse?.network?.[0] + if (tier?.id) { + this.applyLbAlgorithmCapability(tier) + } + }).catch(error => { + console.warn('Failed to enumerate VPC tiers for LB capabilities; using defaults', error) + }) + } + }, + fetchLbCapabilitiesByNetworkId (networkId) { getAPI('listNetworks', { id: networkId, listall: true }).then(json => { const network = json?.listnetworksresponse?.network?.[0] - const lbService = network?.service?.find(s => s.name === 'Lb') - const algoCap = lbService?.capability?.find(c => c.name === 'SupportedLbAlgorithms') - if (algoCap && algoCap.value) { - const algos = algoCap.value.split(',').map(s => s.trim()).filter(s => s) - if (algos.length > 0) { - this.supportedAlgorithms = algos - // If the default algorithm is not in the supported set, switch to the - // first one so the dropdown shows a valid selection from the start. - if (!algos.includes(this.newRule.algorithm)) { - this.newRule.algorithm = algos[0] - } - } + if (network) { + this.applyLbAlgorithmCapability(network) } }).catch(error => { // Swallow - we just keep the default list. Don't pop a notification @@ -1132,6 +1138,21 @@ export default { console.warn('Failed to load LB algorithm capabilities; using defaults', error) }) }, + applyLbAlgorithmCapability (network) { + const lbService = network?.service?.find(s => s.name === 'Lb') + const algoCap = lbService?.capability?.find(c => c.name === 'SupportedLbAlgorithms') + if (algoCap && algoCap.value) { + const algos = algoCap.value.split(',').map(s => s.trim()).filter(s => s) + if (algos.length > 0) { + this.supportedAlgorithms = algos + // If the default algorithm is not in the supported set, switch to the first one + // so the dropdown shows a valid selection from the start. + if (!algos.includes(this.newRule.algorithm)) { + this.newRule.algorithm = algos[0] + } + } + } + }, fetchVpc () { if (!this.resource.vpcid) { return diff --git a/ui/src/views/network/VpcTiersTab.vue b/ui/src/views/network/VpcTiersTab.vue index 4a689f13c34d..bf21c3ec8227 100644 --- a/ui/src/views/network/VpcTiersTab.vue +++ b/ui/src/views/network/VpcTiersTab.vue @@ -347,8 +347,8 @@ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }" > - - {{ key }} + + {{ $t('label.lb.algorithm.' + algo) }} @@ -409,11 +409,13 @@ export default { errorPrivateMtu: '', gatewayPlaceholder: '', netmaskPlaceholder: '', - algorithms: { - Source: 'source', - 'Round-robin': 'roundrobin', - 'Least connections': 'leastconn' - }, + // Default fallback algorithm list for the Internal LB modal. Replaced at runtime by + // fetchLbCapabilitiesForNetwork() with whatever the selected tier's Lb provider + // declares via service.Lb.capability.SupportedLbAlgorithms — same approach used by + // LoadBalancing.vue for the public LB form, so providers like OVN that only support + // {roundrobin, source} expose exactly that subset, while providers that declare the + // full set keep their previous options. + supportedAlgorithms: ['source', 'roundrobin', 'leastconn'], internalLbCols: [ { key: 'name', @@ -728,13 +730,45 @@ export default { this.initForm() this.showAddInternalLB = true this.networkid = id - this.form.algorithm = 'Source' + // Reset to the static default first so the dropdown is never empty while the async + // capability lookup runs; fetchLbCapabilitiesForNetwork() narrows it down once the + // listNetworks response arrives. + this.supportedAlgorithms = ['source', 'roundrobin', 'leastconn'] + this.form.algorithm = this.supportedAlgorithms[0] this.rules = { name: [{ required: true, message: this.$t('message.error.internallb.name') }], sourcePort: [{ required: true, message: this.$t('message.error.internallb.source.port') }], instancePort: [{ required: true, message: this.$t('message.error.internallb.instance.port') }], algorithm: [{ required: true, message: this.$t('label.required') }] } + this.fetchLbCapabilitiesForNetwork(id) + }, + /** + * Loads SupportedLbAlgorithms from the tier network's Lb service. Mirrors the + * fetchLbCapabilities() helper in LoadBalancing.vue so the public-LB form and the + * Internal-LB modal converge on the same data-driven dropdown — provider-agnostic and + * future-proof. Falls back silently to the static default when the lookup fails. + */ + fetchLbCapabilitiesForNetwork (networkId) { + if (!networkId) { + return + } + getAPI('listNetworks', { id: networkId, listall: true }).then(json => { + const network = json?.listnetworksresponse?.network?.[0] + const lbService = network?.service?.find(s => s.name === 'Lb') + const algoCap = lbService?.capability?.find(c => c.name === 'SupportedLbAlgorithms') + if (algoCap && algoCap.value) { + const algos = algoCap.value.split(',').map(s => s.trim()).filter(s => s) + if (algos.length > 0) { + this.supportedAlgorithms = algos + if (!algos.includes(this.form.algorithm)) { + this.form.algorithm = algos[0] + } + } + } + }).catch(error => { + console.warn('Failed to load Internal LB algorithm capabilities; using defaults', error) + }) }, handleAddNetworkSubmit () { if (this.modalLoading) return