From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id B8E591FF141 for ; Tue, 02 Jun 2026 12:03:21 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 79A58E16F; Tue, 2 Jun 2026 12:02:57 +0200 (CEST) From: Daniel Kral To: pve-devel@lists.proxmox.com Subject: [PATCH manager v2 07/12] ui: ha: node affinity: allow setting affinity for node affinity rules Date: Tue, 2 Jun 2026 12:01:11 +0200 Message-ID: <20260602100226.180071-8-d.kral@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260602100226.180071-1-d.kral@proxmox.com> References: <20260602100226.180071-1-d.kral@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1780394515632 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.074 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: DP7KFXHXCJO5C5JIVQLALEA2UPIMP7W2 X-Message-ID-Hash: DP7KFXHXCJO5C5JIVQLALEA2UPIMP7W2 X-MailFrom: d.kral@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Since the node affinity rules can be either 'positive' or 'negative' now, allow users to define either affinity type for the node affinity rules in the web interface as well. Since the priority field does not have any semantic value for negative node affinity rules, do not use the field for the negative affinity type. As the affinity type is switched from positive to negative and vice versa, the node selection is inverted to make the (lossy) conversion easier. The column widths are changed to minimize movement while switching between the affinity types. The inversion is not done if the selection is empty, as preferring all nodes is rather uselessly verbose and avoiding all nodes is disallowed as this rule would not satisfiable. Signed-off-by: Daniel Kral --- changes since v1: - new www/manager6/ha/NodePrioritySelector.js | 65 ++++++++++++++++++- www/manager6/ha/rules/NodeAffinityRuleEdit.js | 25 +++++++ www/manager6/ha/rules/NodeAffinityRules.js | 5 ++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/www/manager6/ha/NodePrioritySelector.js b/www/manager6/ha/NodePrioritySelector.js index ec6ac02a..b58b0ada 100644 --- a/www/manager6/ha/NodePrioritySelector.js +++ b/www/manager6/ha/NodePrioritySelector.js @@ -10,6 +10,16 @@ Ext.define('PVE.forms.NodePrioritySelector', { selectAll: false, isFormField: true, + config: { + useNodePriority: null, + }, + + publishes: ['useNodePriority'], + + viewModel: { + showNodePriority: null, + }, + store: { autoLoad: true, fields: ['node', 'cpu', 'mem', 'priority'], @@ -28,7 +38,7 @@ Ext.define('PVE.forms.NodePrioritySelector', { columns: [ { header: gettext('Node'), - flex: 1, + width: 150, dataIndex: 'node', }, { @@ -42,7 +52,7 @@ Ext.define('PVE.forms.NodePrioritySelector', { header: gettext('CPU usage'), renderer: Proxmox.Utils.render_cpu, sortable: true, - width: 150, + flex: 1, dataIndex: 'cpu', }, { @@ -56,6 +66,14 @@ Ext.define('PVE.forms.NodePrioritySelector', { minValue: 0, maxValue: 1000, isFormField: false, + bind: { + hidden: '{!showNodePriority}', + disabled: '{!showNodePriority}', + }, + }, + bind: { + hidden: '{!showNodePriority}', + disabled: '{!showNodePriority}', }, }, ], @@ -74,6 +92,39 @@ Ext.define('PVE.forms.NodePrioritySelector', { }, }, + invertCheckboxSelection: function () { + let me = this; + + let sm = me.getSelectionModel(); + + let allNodeModels = new Set(sm.getStore().getData().items ?? []); + let selectedNodeModels = new Set(sm.getSelection() ?? []); + + if (!allNodeModels.size || !selectedNodeModels.size) { + return; + } + + sm.deselectAll(); + sm.select([...allNodeModels.difference(selectedNodeModels)]); + }, + + applyUseNodePriority: function (newValue) { + let me = this; + + let oldValue = me.getViewModel().get('showNodePriority'); + + if (newValue !== oldValue) { + me.getViewModel().set('showNodePriority', newValue); + + // Prevent inverting the selection during component initialization + if (oldValue !== null) { + me.invertCheckboxSelection(); + } + } + + return newValue; + }, + getSubmitData: function () { let me = this; let res = {}; @@ -91,7 +142,15 @@ Ext.define('PVE.forms.NodePrioritySelector', { let sm = me.getSelectionModel(); let selectedNodeModels = sm.getSelection() ?? []; let nodes = selectedNodeModels - .map(({ data }) => data.node + (data.priority ? `:${data.priority}` : '')) + .map(({ data }) => { + let nodeEntry = data.node; + + if (me.useNodePriority && data.priority) { + nodeEntry += `:${data.priority}`; + } + + return nodeEntry; + }) .join(','); return nodes; diff --git a/www/manager6/ha/rules/NodeAffinityRuleEdit.js b/www/manager6/ha/rules/NodeAffinityRuleEdit.js index 77da18b1..aecaa276 100644 --- a/www/manager6/ha/rules/NodeAffinityRuleEdit.js +++ b/www/manager6/ha/rules/NodeAffinityRuleEdit.js @@ -1,6 +1,15 @@ Ext.define('PVE.ha.rules.NodeAffinityInputPanel', { extend: 'PVE.ha.RuleInputPanel', + viewModel: { + data: { + affinity: 'positive', + }, + formulas: { + isPositiveNodeAffinity: (get) => get('affinity') === 'positive', + }, + }, + initComponent: function () { let me = this; @@ -18,6 +27,19 @@ Ext.define('PVE.ha.rules.NodeAffinityInputPanel', { uncheckedValue: 0, defaultValue: 0, }, + { + xtype: 'proxmoxKVComboBox', + name: 'affinity', + fieldLabel: gettext('Affinity'), + allowBlank: false, + comboItems: [ + ['positive', gettext('Prefer Nodes')], + ['negative', gettext('Avoid Nodes')], + ], + bind: { + value: '{affinity}', + }, + }, ]; me.columnB = [ @@ -25,6 +47,9 @@ Ext.define('PVE.ha.rules.NodeAffinityInputPanel', { xtype: 'pveNodePrioritySelector', name: 'nodes', allowBlank: false, + bind: { + useNodePriority: '{isPositiveNodeAffinity}', + }, }, ]; diff --git a/www/manager6/ha/rules/NodeAffinityRules.js b/www/manager6/ha/rules/NodeAffinityRules.js index 6fc42799..089dece8 100644 --- a/www/manager6/ha/rules/NodeAffinityRules.js +++ b/www/manager6/ha/rules/NodeAffinityRules.js @@ -11,6 +11,11 @@ Ext.define('PVE.ha.NodeAffinityRulesView', { stateId: 'grid-ha-node-affinity-rules', columns: [ + { + header: gettext('Affinity'), + width: 75, + dataIndex: 'affinity', + }, { header: gettext('Strict'), width: 75, -- 2.47.3