Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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 <a-select-option>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 <noreply@anthropic.com>
  • Loading branch information
msinhore and claude committed Apr 30, 2026
commit da709bb94137793811bc03614ed7337c2f8c8934
5 changes: 5 additions & 0 deletions ui/src/config/section/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
54 changes: 48 additions & 6 deletions ui/src/views/network/LoadBalancing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option value="roundrobin" :label="$t('label.lb.algorithm.roundrobin')">{{ $t('label.lb.algorithm.roundrobin') }}</a-select-option>
<a-select-option value="leastconn" :label="$t('label.lb.algorithm.leastconn')">{{ $t('label.lb.algorithm.leastconn') }}</a-select-option>
<a-select-option value="source" :label="$t('label.lb.algorithm.source')">{{ $t('label.lb.algorithm.source') }}</a-select-option>
<a-select-option v-for="algo in supportedAlgorithms" :key="algo" :value="algo" :label="$t('label.lb.algorithm.' + algo)">{{ $t('label.lb.algorithm.' + algo) }}</a-select-option>
</a-select>
</div>
<div class="form__item">
Expand Down Expand Up @@ -436,9 +434,7 @@
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option value="roundrobin" :label="$t('label.lb.algorithm.roundrobin')">{{ $t('label.lb.algorithm.roundrobin') }}</a-select-option>
<a-select-option value="leastconn" :label="$t('label.lb.algorithm.leastconn')">{{ $t('label.lb.algorithm.leastconn') }}</a-select-option>
<a-select-option value="source" :label="$t('label.lb.algorithm.source')">{{ $t('label.lb.algorithm.source') }}</a-select-option>
<a-select-option v-for="algo in supportedAlgorithms" :key="algo" :value="algo" :label="$t('label.lb.algorithm.' + algo)">{{ $t('label.lb.algorithm.' + algo) }}</a-select-option>
</a-select>
</div>
<div v-if="lbProvider !== 'Netris'" class="edit-rule__item">
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down