* [pve-devel] [PATCH FOLLOW-UP ha-manager/manager v2 0/5] fix rule migration errors and add UI components
@ 2025-07-23 15:35 Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 1/4] config: use entire path to groups.cfg path in delete_group_config Michael Köppl
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Michael Köppl @ 2025-07-23 15:35 UTC (permalink / raw)
To: pve-devel
These patches are meant to extend the first part of v3 of the HA rules
series [0]. During testing, I encountered a couple of errors that broke
the migration and also found that there was no UI for the rules. I
reused parts of the v2 series to supply a UI for node affinity rules. I
marked the corresponding patches accordingly.
[0] https://lore.proxmox.com/pve-devel/20250704181659.465441-1-d.kral@proxmox.com/
Changes since v2 of the follow-up patches:
- Added 2 additional patches to ha-manager regarding the replacement of
the 'state' property with the 'disable' property.
- Add UI part for pve-manager
pve-ha-manager:
Michael Köppl (4):
config: use entire path to groups.cfg path in delete_group_config
ha: decode JSON string with version info returned by get_node_kv
api: return disable field for rules endpoint
ha: check for actual disable value during rule checks
src/PVE/API2/HA/Rules.pm | 1 +
src/PVE/HA/Config.pm | 3 ++-
src/PVE/HA/Env/PVE2.pm | 4 +++-
src/PVE/HA/Rules.pm | 2 +-
4 files changed, 7 insertions(+), 3 deletions(-)
pve-manager:
Michael Köppl (1):
ui: add overview and edit components for node affinity rules
www/manager6/Makefile | 5 +
www/manager6/dc/Config.js | 7 +
www/manager6/ha/Groups.js | 4 +-
www/manager6/ha/RuleEdit.js | 145 +++++++++++++
www/manager6/ha/RuleErrorsModal.js | 50 +++++
www/manager6/ha/Rules.js | 193 ++++++++++++++++++
www/manager6/ha/rules/NodeAffinityRuleEdit.js | 151 ++++++++++++++
www/manager6/ha/rules/NodeAffinityRules.js | 36 ++++
8 files changed, 590 insertions(+), 1 deletion(-)
create mode 100644 www/manager6/ha/RuleEdit.js
create mode 100644 www/manager6/ha/RuleErrorsModal.js
create mode 100644 www/manager6/ha/Rules.js
create mode 100644 www/manager6/ha/rules/NodeAffinityRuleEdit.js
create mode 100644 www/manager6/ha/rules/NodeAffinityRules.js
Summary over all repositories:
12 files changed, 597 insertions(+), 4 deletions(-)
--
Generated by git-murpp 0.8.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* [pve-devel] [PATCH FOLLOW-UP ha-manager v2 1/4] config: use entire path to groups.cfg path in delete_group_config
2025-07-23 15:35 [pve-devel] [PATCH FOLLOW-UP ha-manager/manager v2 0/5] fix rule migration errors and add UI components Michael Köppl
@ 2025-07-23 15:35 ` Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 2/4] ha: decode JSON string with version info returned by get_node_kv Michael Köppl
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Michael Köppl @ 2025-07-23 15:35 UTC (permalink / raw)
To: pve-devel
Prepend /etc/pve/ to the path, as is done for other read and write
functions for the groups.cfg file. Otherwise, removing the file does not
work.
Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
src/PVE/HA/Config.pm | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/PVE/HA/Config.pm b/src/PVE/HA/Config.pm
index 59bafd7..785b4ba 100644
--- a/src/PVE/HA/Config.pm
+++ b/src/PVE/HA/Config.pm
@@ -235,8 +235,9 @@ sub read_group_config {
}
sub delete_group_config {
+ my $groups_config_file = "/etc/pve/$ha_groups_config";
- unlink $ha_groups_config or die "failed to remove group config: $!\n";
+ unlink $groups_config_file or die "failed to remove group config: $!\n";
}
sub write_group_config {
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* [pve-devel] [PATCH FOLLOW-UP ha-manager v2 2/4] ha: decode JSON string with version info returned by get_node_kv
2025-07-23 15:35 [pve-devel] [PATCH FOLLOW-UP ha-manager/manager v2 0/5] fix rule migration errors and add UI components Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 1/4] config: use entire path to groups.cfg path in delete_group_config Michael Köppl
@ 2025-07-23 15:35 ` Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 3/4] api: return disable field for rules endpoint Michael Köppl
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Michael Köppl @ 2025-07-23 15:35 UTC (permalink / raw)
To: pve-devel
The value returned by get_node_kv is a JSON string and has to be decoded
to read the version of the node.
Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
src/PVE/HA/Env/PVE2.pm | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/PVE/HA/Env/PVE2.pm b/src/PVE/HA/Env/PVE2.pm
index aecffc0..0ddc1ac 100644
--- a/src/PVE/HA/Env/PVE2.pm
+++ b/src/PVE/HA/Env/PVE2.pm
@@ -514,7 +514,9 @@ sub get_node_version {
return undef if !$version_info->{$node};
- return $version_info->{$node}->{version};
+ my $node_versioninfo = eval { decode_json($version_info->{$node}) };
+
+ return $node_versioninfo->{version};
}
1;
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* [pve-devel] [PATCH FOLLOW-UP ha-manager v2 3/4] api: return disable field for rules endpoint
2025-07-23 15:35 [pve-devel] [PATCH FOLLOW-UP ha-manager/manager v2 0/5] fix rule migration errors and add UI components Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 1/4] config: use entire path to groups.cfg path in delete_group_config Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 2/4] ha: decode JSON string with version info returned by get_node_kv Michael Köppl
@ 2025-07-23 15:35 ` Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 4/4] ha: check for actual disable value during rule checks Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP manager v2 1/1] ui: add overview and edit components for node affinity rules Michael Köppl
4 siblings, 0 replies; 6+ messages in thread
From: Michael Köppl @ 2025-07-23 15:35 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
src/PVE/API2/HA/Rules.pm | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/PVE/API2/HA/Rules.pm b/src/PVE/API2/HA/Rules.pm
index 2e5e382..817aa1d 100644
--- a/src/PVE/API2/HA/Rules.pm
+++ b/src/PVE/API2/HA/Rules.pm
@@ -25,6 +25,7 @@ my $get_api_ha_rule = sub {
my $rule_cfg = dclone($rules->{ids}->{$ruleid});
+ $rule_cfg->{disable} = $rules->{ids}->{$ruleid}->{disable};
$rule_cfg->{rule} = $ruleid;
$rule_cfg->{digest} = $rules->{digest};
$rule_cfg->{order} = $rules->{order}->{$ruleid};
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* [pve-devel] [PATCH FOLLOW-UP ha-manager v2 4/4] ha: check for actual disable value during rule checks
2025-07-23 15:35 [pve-devel] [PATCH FOLLOW-UP ha-manager/manager v2 0/5] fix rule migration errors and add UI components Michael Köppl
` (2 preceding siblings ...)
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 3/4] api: return disable field for rules endpoint Michael Köppl
@ 2025-07-23 15:35 ` Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP manager v2 1/1] ui: add overview and edit components for node affinity rules Michael Köppl
4 siblings, 0 replies; 6+ messages in thread
From: Michael Köppl @ 2025-07-23 15:35 UTC (permalink / raw)
To: pve-devel
When exclude_disabled_rules was enabled, rules would be excluded from
the check even if the value was actually false, since it still existed.
The check now makes sure that the rule is really disabled before
excluding it.
Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
src/PVE/HA/Rules.pm | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/PVE/HA/Rules.pm b/src/PVE/HA/Rules.pm
index bda0b5d..e756c7d 100644
--- a/src/PVE/HA/Rules.pm
+++ b/src/PVE/HA/Rules.pm
@@ -431,7 +431,7 @@ sub foreach_rule : prototype($$;$) {
next if !$rule; # skip invalid rules
next if defined($sid) && !defined($rule->{resources}->{$sid});
next if defined($type) && $rule->{type} ne $type;
- next if $exclude_disabled_rules && exists($rule->{disable});
+ next if $exclude_disabled_rules && exists($rule->{disable}) && $rule->{disable};
$func->($rule, $ruleid);
}
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* [pve-devel] [PATCH FOLLOW-UP manager v2 1/1] ui: add overview and edit components for node affinity rules
2025-07-23 15:35 [pve-devel] [PATCH FOLLOW-UP ha-manager/manager v2 0/5] fix rule migration errors and add UI components Michael Köppl
` (3 preceding siblings ...)
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 4/4] ha: check for actual disable value during rule checks Michael Köppl
@ 2025-07-23 15:35 ` Michael Köppl
4 siblings, 0 replies; 6+ messages in thread
From: Michael Köppl @ 2025-07-23 15:35 UTC (permalink / raw)
To: pve-devel
Add the rules overview that replaces the groups overview and displays
node affinity rules. In addition, the edit dialogs for node affinity
rules are added, allowing both creation and editing of node affinity
rules.
Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
Originally-by: Daniel Kral <d.kral@proxmox.com>
---
This is based on the v2 version of the series [0], but I incorporated
the changes suggested in the cover letter of v3 and the review comments
on v2. I added this since this patch was mentioned in manager 2/3 [1]
of v3, but it seems it was accidentally left out during splitting up
the series. Based on this, the rules can be seen and manipulated after
successful migration from groups to rules. The first patch of part 2 of
the v3 series makes changes to the files added in this patch and should
apply cleanly.
www/manager6/Makefile | 5 +
www/manager6/dc/Config.js | 7 +
www/manager6/ha/Groups.js | 4 +-
www/manager6/ha/RuleEdit.js | 145 +++++++++++++
www/manager6/ha/RuleErrorsModal.js | 50 +++++
www/manager6/ha/Rules.js | 193 ++++++++++++++++++
www/manager6/ha/rules/NodeAffinityRuleEdit.js | 151 ++++++++++++++
www/manager6/ha/rules/NodeAffinityRules.js | 36 ++++
8 files changed, 590 insertions(+), 1 deletion(-)
create mode 100644 www/manager6/ha/RuleEdit.js
create mode 100644 www/manager6/ha/RuleErrorsModal.js
create mode 100644 www/manager6/ha/Rules.js
create mode 100644 www/manager6/ha/rules/NodeAffinityRuleEdit.js
create mode 100644 www/manager6/ha/rules/NodeAffinityRules.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 84a8b4d00..dc0291c54 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -148,8 +148,13 @@ JSSRC= \
ha/Groups.js \
ha/ResourceEdit.js \
ha/Resources.js \
+ ha/RuleEdit.js \
+ ha/RuleErrorsModal.js \
+ ha/Rules.js \
ha/Status.js \
ha/StatusView.js \
+ ha/rules/NodeAffinityRuleEdit.js \
+ ha/rules/NodeAffinityRules.js \
dc/ACLView.js \
dc/ACMEClusterView.js \
dc/AuthEditBase.js \
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 76c9a6ca1..5140efec6 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -176,6 +176,13 @@ Ext.define('PVE.dc.Config', {
iconCls: 'fa fa-object-group',
itemId: 'ha-groups',
},
+ {
+ title: gettext('Rules'),
+ groups: ['ha'],
+ xtype: 'pveHARulesView',
+ iconCls: 'fa fa-gears',
+ itemId: 'ha-rules',
+ },
{
title: gettext('Fencing'),
groups: ['ha'],
diff --git a/www/manager6/ha/Groups.js b/www/manager6/ha/Groups.js
index 6b4958f01..6c46a40fa 100644
--- a/www/manager6/ha/Groups.js
+++ b/www/manager6/ha/Groups.js
@@ -58,7 +58,7 @@ Ext.define('PVE.ha.GroupsView', {
tbar: [
{
text: gettext('Create'),
- disabled: !caps.nodes['Sys.Console'],
+ disabled: true,
handler: function () {
Ext.create('PVE.ha.GroupEdit', {
listeners: {
@@ -112,6 +112,8 @@ Ext.define('PVE.ha.GroupsView', {
},
});
+ me.emptyText = gettext('HA Node Affinity rules are used instead of HA Groups');
+
me.callParent();
},
});
diff --git a/www/manager6/ha/RuleEdit.js b/www/manager6/ha/RuleEdit.js
new file mode 100644
index 000000000..5bfe042ef
--- /dev/null
+++ b/www/manager6/ha/RuleEdit.js
@@ -0,0 +1,145 @@
+Ext.define('PVE.ha.RuleInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+
+ onlineHelp: 'ha_manager_rules',
+
+ formatServiceListString: function (resources) {
+ let me = this;
+
+ return resources.map((vmid) => {
+ if (me.resourcesStore.getById(`qemu/${vmid}`)) {
+ return `vm:${vmid}`;
+ } else if (me.resourcesStore.getById(`lxc/${vmid}`)) {
+ return `ct:${vmid}`;
+ } else {
+ Ext.Msg.alert(gettext('Error'), `Could not find resource type for ${vmid}`);
+ throw `Unknown resource type: ${vmid}`;
+ }
+ });
+ },
+
+ onGetValues: function (values) {
+ let me = this;
+
+ values.type = me.ruleType;
+
+ if (!me.isCreate) {
+ delete values.rule;
+ }
+
+ values.disable = 1 - values.enabled;
+ delete values.enabled;
+
+ values.resources = me.formatServiceListString(values.resources);
+
+ return values;
+ },
+
+ initComponent: function () {
+ let me = this;
+
+ let resourcesStore = Ext.create('Ext.data.Store', {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ filters: [
+ {
+ property: 'type',
+ value: /lxc|qemu/,
+ },
+ {
+ property: 'hastate',
+ operator: '!=',
+ value: 'unmanaged',
+ },
+ ],
+ });
+
+ Ext.apply(me, {
+ resourcesStore: resourcesStore,
+ });
+
+ me.column1.unshift(
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'rule',
+ value: me.ruleId || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'vmComboSelector',
+ name: 'resources',
+ fieldLabel: gettext('Resources'),
+ store: me.resourcesStore,
+ allowBlank: false,
+ autoSelect: false,
+ multiSelect: true,
+ validateExists: true,
+ },
+ );
+
+ me.column2 = me.column2 ?? [];
+
+ me.column2.unshift({
+ xtype: 'proxmoxcheckbox',
+ name: 'enabled',
+ fieldLabel: gettext('Enable'),
+ uncheckedValue: 0,
+ defaultValue: 1,
+ checked: true,
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.ha.RuleEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ defaultFocus: undefined, // prevent the vmComboSelector to be expanded when focusing the window
+
+ initComponent: function () {
+ let me = this;
+
+ me.isCreate = !me.ruleId;
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/cluster/ha/rules';
+ me.method = 'POST';
+ } else {
+ me.url = `/api2/extjs/cluster/ha/rules/${me.ruleId}`;
+ me.method = 'PUT';
+ }
+
+ let inputPanel = Ext.create(me.panelType, {
+ ruleId: me.ruleId,
+ ruleType: me.ruleType,
+ isCreate: me.isCreate,
+ });
+
+ Ext.apply(me, {
+ subject: me.panelName,
+ isAdd: true,
+ items: [inputPanel],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: (response, options) => {
+ let values = response.result.data;
+
+ values.resources = values.resources
+ .split(',')
+ .map((service) => service.split(':')[1]);
+
+ values.enabled = !values.disable;
+
+ inputPanel.setValues(values);
+ },
+ });
+ }
+ },
+});
diff --git a/www/manager6/ha/RuleErrorsModal.js b/www/manager6/ha/RuleErrorsModal.js
new file mode 100644
index 000000000..aac1ef873
--- /dev/null
+++ b/www/manager6/ha/RuleErrorsModal.js
@@ -0,0 +1,50 @@
+Ext.define('PVE.ha.RuleErrorsModal', {
+ extend: 'Ext.window.Window',
+ alias: ['widget.pveHARulesErrorsModal'],
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ modal: true,
+ scrollable: true,
+ resizable: false,
+
+ title: gettext('Rule errors'),
+
+ initComponent: function () {
+ let me = this;
+
+ let renderHARuleErrors = (errors) => {
+ if (!errors) {
+ return gettext('HA Rule has no errors.');
+ }
+
+ let errorListItemsHtml = '';
+
+ for (let [opt, messages] of Object.entries(errors)) {
+ errorListItemsHtml += messages
+ .map((message) => `<li>${Ext.htmlEncode(`${opt}: ${message}`)}</li>`)
+ .join('');
+ }
+
+ return `<div>
+ <p>${gettext('The HA rule has the following errors:')}</p>
+ <ul>${errorListItemsHtml}</ul>
+ </div>`;
+ };
+
+ Ext.apply(me, {
+ modal: true,
+ border: false,
+ layout: 'fit',
+ items: [
+ {
+ xtype: 'displayfield',
+ padding: 20,
+ scrollable: true,
+ value: renderHARuleErrors(me.errors),
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
diff --git a/www/manager6/ha/Rules.js b/www/manager6/ha/Rules.js
new file mode 100644
index 000000000..ef861a3ff
--- /dev/null
+++ b/www/manager6/ha/Rules.js
@@ -0,0 +1,193 @@
+Ext.define('PVE.ha.RulesBaseView', {
+ extend: 'Ext.grid.GridPanel',
+
+ initComponent: function () {
+ let me = this;
+
+ if (!me.ruleType) {
+ throw 'no rule type given';
+ }
+
+ let store = new Ext.data.Store({
+ model: 'pve-ha-rules',
+ autoLoad: true,
+ filters: [
+ {
+ property: 'type',
+ value: me.ruleType,
+ },
+ ],
+ });
+
+ let reloadStore = () => store.load();
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let createRuleEditWindow = (ruleId) => {
+ if (!me.inputPanel) {
+ throw `no editor registered for ha rule type: ${me.ruleType}`;
+ }
+
+ Ext.create('PVE.ha.RuleEdit', {
+ panelType: `PVE.ha.rules.${me.inputPanel}`,
+ panelName: me.ruleTitle,
+ ruleType: me.ruleType,
+ ruleId: ruleId,
+ autoShow: true,
+ listeners: {
+ destroy: reloadStore,
+ },
+ });
+ };
+
+ let runEditor = () => {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ let { rule } = rec.data;
+ createRuleEditWindow(rule);
+ };
+
+ let editButton = Ext.create('Proxmox.button.Button', {
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: runEditor,
+ });
+
+ let removeButton = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/cluster/ha/rules/',
+ callback: reloadStore,
+ });
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ viewConfig: {
+ trackOver: false,
+ },
+ emptyText: Ext.String.format(gettext('No {0} rules configured.'), me.ruleTitle),
+ tbar: [
+ {
+ text: gettext('Add'),
+ handler: () => createRuleEditWindow(),
+ },
+ editButton,
+ removeButton,
+ ],
+ listeners: {
+ activate: reloadStore,
+ itemdblclick: runEditor,
+ },
+ });
+
+ me.columns.unshift(
+ {
+ header: gettext('Enabled'),
+ xtype: 'actioncolumn',
+ width: 65,
+ align: 'center',
+ dataIndex: 'disable',
+ items: [
+ {
+ isActionDisabled: (table, rowIndex, colIndex, item, { data }) =>
+ !data.errors,
+ handler: (table, rowIndex, colIndex, item, event, { data }) => {
+ Ext.create('PVE.ha.RuleErrorsModal', {
+ autoShow: true,
+ errors: data.errors ?? {},
+ });
+ },
+ getTip: (value, _m, { data }) => {
+ if (data.errors) {
+ return gettext('Errors');
+ }
+
+ if (!value) {
+ return gettext('Enabled');
+ } else {
+ return gettext('Disabled');
+ }
+ },
+ getClass: (value, _m, { data }) => {
+ let iconName = 'check';
+
+ if (data.errors) {
+ iconName = 'exclamation-triangle';
+ } else if (value) {
+ iconName = 'minus';
+ }
+
+ return `fa fa-${iconName}`;
+ },
+ },
+ ],
+ },
+ {
+ header: gettext('Rule'),
+ width: 200,
+ dataIndex: 'rule',
+ },
+ );
+
+ me.columns.push({
+ header: gettext('Comment'),
+ flex: 1,
+ renderer: Ext.String.htmlEncode,
+ dataIndex: 'comment',
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define(
+ 'PVE.ha.RulesView',
+ {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveHARulesView',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'ha_manager_rules',
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+
+ items: [
+ {
+ title: gettext('HA Node Affinity'),
+ xtype: 'pveHANodeAffinityRulesView',
+ flex: 1,
+ border: 0,
+ },
+ ],
+ },
+ function () {
+ Ext.define('pve-ha-rules', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'rule',
+ 'type',
+ 'nodes',
+ 'errors',
+ 'disable',
+ 'comment',
+ 'resources',
+ {
+ name: 'strict',
+ type: 'boolean',
+ },
+ 'digest',
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/ha/rules',
+ },
+ idProperty: 'rule',
+ });
+ },
+);
diff --git a/www/manager6/ha/rules/NodeAffinityRuleEdit.js b/www/manager6/ha/rules/NodeAffinityRuleEdit.js
new file mode 100644
index 000000000..497831f7b
--- /dev/null
+++ b/www/manager6/ha/rules/NodeAffinityRuleEdit.js
@@ -0,0 +1,151 @@
+Ext.define('PVE.ha.rules.NodeAffinityInputPanel', {
+ extend: 'PVE.ha.RuleInputPanel',
+
+ initComponent: function () {
+ let me = this;
+
+ me.column1 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'strict',
+ fieldLabel: gettext('Strict'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Enable if the resources must be restricted to the nodes.'),
+ },
+ uncheckedValue: 0,
+ defaultValue: 0,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Comment'),
+ name: 'comment',
+ allowBlank: true,
+ },
+ ];
+
+ /* TODO Code copied from GroupEdit, should be factored out in component */
+ let update_nodefield, update_node_selection;
+
+ let sm = Ext.create('Ext.selection.CheckboxModel', {
+ mode: 'SIMPLE',
+ listeners: {
+ selectionchange: function (model, selected) {
+ update_nodefield(selected);
+ },
+ },
+ });
+
+ let store = Ext.create('Ext.data.Store', {
+ fields: ['node', 'mem', 'cpu', 'priority'],
+ data: PVE.data.ResourceStore.getNodes(), // use already cached data to avoid an API call
+ proxy: {
+ type: 'memory',
+ reader: { type: 'json' },
+ },
+ sorters: [
+ {
+ property: 'node',
+ direction: 'ASC',
+ },
+ ],
+ });
+
+ var nodegrid = Ext.createWidget('grid', {
+ store: store,
+ border: true,
+ height: 300,
+ selModel: sm,
+ columns: [
+ {
+ header: gettext('Node'),
+ flex: 1,
+ dataIndex: 'node',
+ },
+ {
+ header: gettext('Memory usage') + ' %',
+ renderer: PVE.Utils.render_mem_usage_percent,
+ sortable: true,
+ width: 150,
+ dataIndex: 'mem',
+ },
+ {
+ header: gettext('CPU usage'),
+ renderer: Proxmox.Utils.render_cpu,
+ sortable: true,
+ width: 150,
+ dataIndex: 'cpu',
+ },
+ {
+ header: gettext('Priority'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'priority',
+ sortable: true,
+ stopSelection: true,
+ widget: {
+ xtype: 'proxmoxintegerfield',
+ minValue: 0,
+ maxValue: 1000,
+ isFormField: false,
+ listeners: {
+ change: function (numberfield, value, old_value) {
+ let record = numberfield.getWidgetRecord();
+ record.set('priority', value);
+ update_nodefield(sm.getSelection());
+ record.commit();
+ },
+ },
+ },
+ },
+ ],
+ });
+
+ let nodefield = Ext.create('Ext.form.field.Hidden', {
+ name: 'nodes',
+ value: '',
+ listeners: {
+ change: function (field, value) {
+ update_node_selection(value);
+ },
+ },
+ isValid: function () {
+ let value = this.getValue();
+ return value && value.length !== 0;
+ },
+ });
+
+ update_node_selection = function (string) {
+ sm.deselectAll(true);
+
+ string.split(',').forEach(function (e, idx, array) {
+ let [node, priority] = e.split(':');
+ store.each(function (record) {
+ if (record.get('node') === node) {
+ sm.select(record, true);
+ record.set('priority', priority);
+ record.commit();
+ }
+ });
+ });
+ nodegrid.reconfigure(store);
+ };
+
+ update_nodefield = function (selected) {
+ let nodes = selected
+ .map(({ data }) => data.node + (data.priority ? `:${data.priority}` : ''))
+ .join(',');
+
+ // nodefield change listener calls us again, which results in a
+ // endless recursion, suspend the event temporary to avoid this
+ nodefield.suspendEvent('change');
+ nodefield.setValue(nodes);
+ nodefield.resumeEvent('change');
+ };
+
+ me.column2 = [nodefield];
+
+ me.columnB = [nodegrid];
+
+ me.callParent();
+ },
+});
diff --git a/www/manager6/ha/rules/NodeAffinityRules.js b/www/manager6/ha/rules/NodeAffinityRules.js
new file mode 100644
index 000000000..6bac4d7d9
--- /dev/null
+++ b/www/manager6/ha/rules/NodeAffinityRules.js
@@ -0,0 +1,36 @@
+Ext.define('PVE.ha.NodeAffinityRulesView', {
+ extend: 'PVE.ha.RulesBaseView',
+ alias: 'widget.pveHANodeAffinityRulesView',
+
+ ruleType: 'node-affinity',
+ ruleTitle: gettext('HA Node Affinity'),
+ inputPanel: 'NodeAffinityInputPanel',
+ faIcon: 'map-pin',
+
+ stateful: true,
+ stateId: 'grid-ha-node-affinity-rules',
+
+ initComponent: function () {
+ let me = this;
+
+ me.columns = [
+ {
+ header: gettext('Strict'),
+ width: 50,
+ dataIndex: 'strict',
+ },
+ {
+ header: gettext('Resources'),
+ flex: 1,
+ dataIndex: 'resources',
+ },
+ {
+ header: gettext('Nodes'),
+ flex: 1,
+ dataIndex: 'nodes',
+ },
+ ];
+
+ me.callParent();
+ },
+});
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2025-07-23 15:34 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-07-23 15:35 [pve-devel] [PATCH FOLLOW-UP ha-manager/manager v2 0/5] fix rule migration errors and add UI components Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 1/4] config: use entire path to groups.cfg path in delete_group_config Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 2/4] ha: decode JSON string with version info returned by get_node_kv Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 3/4] api: return disable field for rules endpoint Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP ha-manager v2 4/4] ha: check for actual disable value during rule checks Michael Köppl
2025-07-23 15:35 ` [pve-devel] [PATCH FOLLOW-UP manager v2 1/1] ui: add overview and edit components for node affinity rules Michael Köppl
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox