From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <l.nunner@proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by lists.proxmox.com (Postfix) with ESMTPS id 52378C158
 for <pve-devel@lists.proxmox.com>; Thu, 14 Sep 2023 12:03:55 +0200 (CEST)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
 by firstgate.proxmox.com (Proxmox) with ESMTP id B4F6236F3D
 for <pve-devel@lists.proxmox.com>; Thu, 14 Sep 2023 12:03:54 +0200 (CEST)
Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com
 [94.136.29.106])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by firstgate.proxmox.com (Proxmox) with ESMTPS
 for <pve-devel@lists.proxmox.com>; Thu, 14 Sep 2023 12:03:53 +0200 (CEST)
Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1])
 by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 1AEA347206
 for <pve-devel@lists.proxmox.com>; Thu, 14 Sep 2023 12:03:53 +0200 (CEST)
From: Leo Nunner <l.nunner@proxmox.com>
To: pve-devel@lists.proxmox.com
Date: Thu, 14 Sep 2023 12:03:41 +0200
Message-Id: <20230914100341.122329-5-l.nunner@proxmox.com>
X-Mailer: git-send-email 2.39.2
In-Reply-To: <20230914100341.122329-1-l.nunner@proxmox.com>
References: <20230914100341.122329-1-l.nunner@proxmox.com>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
X-SPAM-LEVEL: Spam detection results:  0
 AWL -0.089 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
Subject: [pve-devel] [PATCH WIP manager 2/2] gui: configure cluster-wide
 hosts through gui
X-BeenThere: pve-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox VE development discussion <pve-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/>
List-Post: <mailto:pve-devel@lists.proxmox.com>
List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=subscribe>
X-List-Received-Date: Thu, 14 Sep 2023 10:03:55 -0000

GUI components for the cluster-wide hosts configuration. It is located
under Datacenter > Hosts, and allows editing, adding and deleting
entries. They are searchable, and the edit window allows adding a
variable number of hostnames to each IP address. There is a separate
"apply" button, which starts a task to sync the hosts config to each
node in the cluster.

Signed-off-by: Leo Nunner <l.nunner@proxmox.com>
---
 www/manager6/Makefile     |   1 +
 www/manager6/dc/Config.js |   6 +
 www/manager6/dc/Hosts.js  | 257 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 264 insertions(+)
 create mode 100644 www/manager6/dc/Hosts.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 59a5d8a7f..f34d5b2f2 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -157,6 +157,7 @@ JSSRC= 							\
 	dc/GroupView.js					\
 	dc/Guests.js					\
 	dc/Health.js					\
+	dc/Hosts.js					\
 	dc/Log.js					\
 	dc/NodeView.js					\
 	dc/NotificationEvents.js			\
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 9ba7b301f..332662516 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -178,6 +178,12 @@ Ext.define('PVE.dc.Config', {
 		iconCls: 'fa fa-bolt',
 		xtype: 'pveFencingView',
 		itemId: 'ha-fencing',
+	    },
+	    {
+		xtype: 'pveClusterHosts',
+		title: gettext('Hosts'),
+		iconCls: 'fa fa-globe',
+		itemId: 'hosts',
 	    });
 	    // always show on initial load, will be hiddea later if the SDN API calls don't exist,
 	    // else it won't be shown at first if the user initially loads with DC selected
diff --git a/www/manager6/dc/Hosts.js b/www/manager6/dc/Hosts.js
new file mode 100644
index 000000000..57bf29054
--- /dev/null
+++ b/www/manager6/dc/Hosts.js
@@ -0,0 +1,257 @@
+Ext.define('pve-etc-hosts-entry', {
+    extend: 'Ext.data.Model',
+    fields: [
+	{ name: 'ip', type: 'string' },
+	{ name: 'hosts', type: 'string' },
+	{ name: 'comment', type: 'string' },
+    ],
+});
+
+Ext.define('PVE.dc.HostsEditWindow', {
+    extend: 'Proxmox.window.Edit',
+
+    width: 600,
+
+    ip: undefined,
+
+    initComponent: function() {
+	var me = this;
+
+	Ext.apply(me, {
+	    subject: "Hosts entry",
+	    defaultFocus: 'textfield[name=ip]',
+	});
+
+	Ext.apply(me, {
+	    items: [
+		{
+		    xtype: 'textfield',
+		    fieldLabel: gettext('IP'),
+		    name: 'ip',
+		    dataIndex: 'ip',
+		    allowBlank: false,
+		    vtype: 'IP64Address',
+		},
+		{
+		    xtype: 'textfield',
+		    fieldLabel: gettext('Comment'),
+		    name: 'comment',
+		    dataIndex: 'comment',
+		    allowBlank: true,
+		},
+		{
+		    xtype: 'fieldcontainer',
+		    fieldLabel: gettext('Hostnames'),
+		    items: [
+			{
+			    xtype: 'proxmoxHostsEditPanel', name: 'hosts',
+			},
+		    ],
+		},
+	    ],
+	});
+
+	let base_url = `/api2/extjs/cluster/hosts`;
+	if (me.isCreate) {
+            me.url = base_url;
+            me.method = 'POST';
+        } else {
+            me.url = base_url + `/${me.ip}`;
+            me.method = 'PUT';
+        }
+
+	me.callParent();
+
+	let hostsPanel = me.down("proxmoxHostsEditPanel");
+	let hostsController = hostsPanel.getController();
+
+	me.setValues({ ip: me.ip });
+
+	if (!me.isCreate) { // do we already have data?
+	    me.load();
+	} else { // if not, we create a single empty host entry
+	    hostsController.addHost();
+	}
+    },
+});
+
+Ext.define('PVE.dc.ClusterHosts', {
+    extend: 'Ext.grid.Panel',
+    xtype: 'pveClusterHosts',
+
+    reload: function() {
+	let me = this;
+	let view = me.getView();
+	view.store.load();
+    },
+
+    layout: 'fit',
+
+    initComponent: function() {
+	let me = this;
+
+	me.store = Ext.create('Ext.data.Store', {
+	    model: 'pve-etc-hosts-entry',
+	    proxy: {
+                type: 'proxmox',
+                url: `/api2/json/cluster/hosts`,
+	    },
+	    sorters: [
+		{
+		    property: 'ip',
+		    direction: 'ASC',
+		},
+	    ],
+	});
+
+	var sm = Ext.create('Ext.selection.RowModel', {});
+
+	var run_editor = function() {
+	    var rec = sm.getSelection()[0];
+	    if (!rec || !(rec.data.ip || rec.data.hosts)) {
+		return;
+	    }
+
+	    let win = Ext.create('PVE.dc.HostsEditWindow', {
+		isCreate: false,
+		ip: rec.data.ip,
+	    });
+	    win.on('destroy', me.reload, me);
+	    win.show();
+	};
+
+	Ext.apply(me, {
+	    tbar: [
+		{
+		    text: gettext('Apply'),
+		    itemId: 'applybtn',
+		    handler: function() {
+			Proxmox.Utils.API2Request({
+			    method: 'PUT',
+			    url: `/cluster/hosts`,
+			    waitMsgTarget: me,
+			    success: function(response, opts) {
+				me.reload();
+			    },
+			    failure: function(response, opts) {
+				Ext.Msg.alert('Error', response.htmlStatus);
+			    },
+			});
+		    },
+		},
+		'|',
+		{
+		    text: gettext('Add'),
+		    itemId: 'addbtn',
+		    handler: function() {
+			let win = Ext.create('PVE.dc.HostsEditWindow', {
+			    isCreate: true,
+			    ip: undefined,
+			});
+			win.on('destroy', me.reload, me);
+			win.show();
+		    },
+		},
+		{
+		    text: gettext('Edit'),
+		    itemId: 'editbtn',
+		    handler: run_editor,
+		},
+		{
+		    text: gettext('Delete'),
+		    itemId: 'deletebtn',
+		    handler: function() {
+			let rec = sm.getSelection()[0];
+
+			Proxmox.Utils.API2Request({
+			    method: 'DELETE',
+			    url: `/cluster/hosts/${rec.data.ip}`,
+			    waitMsgTarget: me,
+			    success: function(response, opts) {
+				me.reload();
+			    },
+			    failure: function(response, opts) {
+				Ext.Msg.alert('Error', response.htmlStatus);
+			    },
+			});
+		    },
+		},
+		'->',
+		gettext('Search') + ':',
+		' ',
+		{
+		    xtype: 'textfield',
+		    width: 200,
+		    enableKeyEvents: true,
+		    emptyText: gettext("IP, FQDN"),
+		    listeners: {
+			keyup: {
+			    buffer: 500,
+			    fn: function(field) {
+				let needle = field.getValue().toLocaleLowerCase();
+				me.store.clearFilter(true);
+				me.store.filter([
+				    {
+					filterFn: ({ data }) =>
+					data.ip?.toLocaleLowerCase().includes(needle) ||
+					data.hosts?.toLocaleLowerCase().includes(needle),
+				    },
+				]);
+			    },
+			},
+			change: function(field, newValue, oldValue) {
+			    if (newValue !== this.originalValue) {
+				this.triggers.clear.setVisible(true);
+			    }
+			},
+		    },
+		    triggers: {
+			clear: {
+			    cls: 'pmx-clear-trigger',
+			    weight: -1,
+			    hidden: true,
+			    handler: function() {
+				this.triggers.clear.setVisible(false);
+				this.setValue(this.originalValue);
+				me.store.clearFilter();
+			    },
+			},
+		    },
+		},
+	    ],
+	});
+
+	Ext.apply(me, {
+	    bodystyle: {
+		width: '100% !important',
+	    },
+	    columns: [
+		{
+		    text: gettext('IP'),
+		    dataIndex: 'ip',
+		    width: 150,
+		},
+		{
+		    text: gettext('Hosts'),
+		    dataIndex: 'hosts',
+		    flex: 1,
+		},
+		{
+		    text: gettext('Comment'),
+		    dataIndex: 'comment',
+		    flex: 1,
+		},
+	    ],
+	});
+
+	Ext.apply(me, {
+	    selModel: sm,
+	    listeners: {
+		itemdblclick: run_editor,
+	    },
+	});
+
+	me.callParent();
+	me.reload();
+    },
+});
-- 
2.39.2