From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 77DB41FF0DF for ; Fri, 28 Aug 2026 15:33:12 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 90E1D217A1; Fri, 28 Aug 2026 15:31:16 +0200 (CEST) From: Shannon Sterz To: pve-devel@lists.proxmox.com Subject: [PATCH manager 13/21] ui: dc: add panel for managing host backup jobs Date: Fri, 28 Aug 2026 15:30:22 +0200 Message-ID: <20260828133030.351140-14-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260828133030.351140-1-s.sterz@proxmox.com> References: <20260828133030.351140-1-s.sterz@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1787923824826 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.688 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_2 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_4 0.1 random spam to be learned in bayes RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust 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: TTXGRATOD2HMJIPAJILEHUOJ74U2UXDI X-Message-ID-Hash: TTXGRATOD2HMJIPAJILEHUOJ74U2UXDI X-MailFrom: s.sterz@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: host backup jobs can be created, edited, removed and run. currently the parameters "additional-files" and "hooks" are not exposed through the ui. also adds task description for host backup jobs. Signed-off-by: Shannon Sterz --- www/manager6/Makefile | 1 + www/manager6/Utils.js | 1 + www/manager6/dc/Config.js | 6 + www/manager6/dc/HostBackupJobs.js | 360 ++++++++++++++++++++++++++++++ 4 files changed, 368 insertions(+) create mode 100644 www/manager6/dc/HostBackupJobs.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 37f29747d..fd41c0e81 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -183,6 +183,7 @@ JSSRC= \ dc/GroupView.js \ dc/Guests.js \ dc/Health.js \ + dc/HostBackupJobs.js \ dc/Log.js \ dc/NodeView.js \ dc/OptionView.js \ diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js index c86a00c5e..46dbb9257 100644 --- a/www/manager6/Utils.js +++ b/www/manager6/Utils.js @@ -2176,6 +2176,7 @@ Ext.define('PVE.Utils', { hashutdown: ['HA', gettext('Shutdown')], hastart: ['HA', gettext('Start')], hastop: ['HA', gettext('Stop')], + 'host-backup': ['', gettext('Host Backup')], imgcopy: ['', gettext('Copy data')], imgdel: ['', gettext('Erase data')], lvmcreate: [gettext('LVM Storage'), gettext('Create')], diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js index 83c48c3d6..d40256632 100644 --- a/www/manager6/dc/Config.js +++ b/www/manager6/dc/Config.js @@ -140,6 +140,12 @@ Ext.define('PVE.dc.Config', { title: gettext('Backup'), itemId: 'backup', }, + { + xtype: 'pveDcHostBackup', + iconCls: 'fa fa-clock-o', + title: gettext('Host Backup Jobs'), + itemId: 'hostBackup', + }, { xtype: 'pveReplicaView', iconCls: 'fa fa-retweet', diff --git a/www/manager6/dc/HostBackupJobs.js b/www/manager6/dc/HostBackupJobs.js new file mode 100644 index 000000000..2576c9e9e --- /dev/null +++ b/www/manager6/dc/HostBackupJobs.js @@ -0,0 +1,360 @@ +Ext.define('PVE.dc.AddHostBackupJob', { + extend: 'Proxmox.window.Edit', + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Host Backup Job'), + + showTaskViewer: true, + jobid: undefined, + + url: '/api2/extjs/cluster/jobs/host-backup', + method: 'POST', + isCreate: true, + + cbindData: function () { + let me = this; + + if (me.jobid) { + me.isCreate = false; + me.method = 'PUT'; + me.url += `/${me.jobid}`; + } + + return {}; + }, + + items: [ + { + xtype: 'inputpanel', + column1: [ + { + xtype: 'pveCalendarEvent', + fieldLabel: gettext('Schedule'), + allowBlank: false, + name: 'schedule', + }, + { + xtype: 'pveNodeSelector', + name: 'nodes', + fieldLabel: gettext('Nodes'), + multiSelect: true, + autoSelect: false, + emptyText: `-- ${gettext('All')} --`, + listeners: { + change: 'nodeChange', + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enable'), + name: 'enabled', + uncheckedValue: 0, + defaultValue: 1, + checked: true, + }, + ], + column2: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Repeat Missed'), + name: 'repeat-missed', + uncheckedValue: 0, + defaultValue: 0, + checked: false, + }, + { + fieldLabel: gettext('Storage'), + name: 'storage', + xtype: 'pveStorageSelector', + reference: 'backupStorageSelector', + clusterView: true, + storageContent: 'backup', + allowBlank: false, + autoSelect: false, + }, + ], + columnB: [ + { + xtype: 'proxmoxtextfield', + name: 'comment', + fieldLabel: gettext('Job Comment'), + cbind: { + deleteEmpty: '{!isCreate}', + }, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Description of the job.'), + }, + }, + // TODO: add custom files + // TODO: maybe hook scripts? + ], + }, + ], + + controller: { + nodeChange: (element, value) => { + let view = element.up('window'); + let store = view.lookup('backupStorageSelector').getStore(); + let id = 'nodeFilter'; + store.removeFilter(id); + + if (value.length !== 0) { + store.addFilter({ + id, + filterFn: (item) => { + if (!item.data.nodes) { + // the storage is valid for all nodes + return true; + } + + let storageNodes = item.data.nodes.split(','); + return value.every((n) => storageNodes.includes(n)); + }, + }); + } else { + store.addFilter({ + id, + // if no nodes are selected the job runs on all nodes, so + // the storage also needs to be available on all nodes. a + // storage is available on all nodes if the `nodes` + // property is not set. + filterFn: (item) => !item.data.nodes, + }); + } + }, + init: function (view) { + let me = this; + + let store = me.lookup('backupStorageSelector').getStore(); + store.addFilter({ filterFn: (item) => item.data.type === 'pbs' }); + store.addFilter({ id: 'nodeFilter', filterFn: (item) => !item.data.nodes }); + + if (!view.isCreate) { + view.load({ + success: function (response, _options) { + view.setValues(response.result.data); + }, + }); + } + }, + }, +}); + +Ext.define('PVE.dc.HostBackup', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveDcHostBackup'], + mixins: ['Proxmox.Mixin.CBind'], + + //onlineHelp: 'chapter_vzdump', + + stateful: true, + stateId: 'grid-dc-node-backup', + + store: { + model: '', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/jobs/host-backup', + }, + }, + + tbar: [ + { + text: gettext('Add'), + xtype: 'button', + handler: 'createBackupJobHandler', + }, + '-', + { + text: gettext('Remove'), + xtype: 'proxmoxStdRemoveButton', + dangerous: true, + customConfirmationMessage: gettext( + 'Are you sure you want to remove the job {0}. This will remove it.', + ), + getRecordName: (rec) => rec.data.id, + callback: function () { + this.up('grid').getStore().load(); + }, + getUrl: (rec) => `/cluster/jobs/host-backup/${rec.data.id}`, + }, + { + text: gettext('Edit'), + xtype: 'proxmoxButton', + handler: 'editNotesHandler', + disabled: true, + }, + '-', + { + text: gettext('Run now'), + xtype: 'proxmoxButton', + handler: 'runNowHandler', + disabled: true, + }, + ], + + listeners: { + itemdblclick: 'dblclickHandler', + }, + + columns: [ + { + header: gettext('Enabled'), + width: 80, + dataIndex: 'enabled', + align: 'center', + renderer: Proxmox.Utils.renderEnabledIcon, + sortable: true, + }, + { + header: gettext('ID'), + dataIndex: 'id', + hidden: true, + }, + { + header: gettext('Nodes'), + width: 100, + sortable: true, + dataIndex: 'nodes', + renderer: (value) => value ?? `-- ${gettext('All')} --`, + }, + { + header: gettext('Schedule'), + width: 150, + dataIndex: 'schedule', + }, + { + text: gettext('Next Run'), + dataIndex: 'next-run', + width: 150, + renderer: PVE.Utils.render_next_event, + }, + { + header: gettext('Storage'), + width: 100, + sortable: true, + dataIndex: 'storage', + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.htmlEncode, + sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''), + flex: 1, + }, + ], + + controller: { + createBackupJobHandler: (button, _event) => { + let grid = button.up('grid'); + Ext.create('PVE.dc.AddHostBackupJob', { + isCreate: true, + autoShow: true, + listeners: { close: () => grid.getStore().load() }, + }); + }, + editNotesHandler: function (button, _event, rec) { + if (!rec) { + console.warn('No host backup selected!'); + } else { + this.dblclickHandler(button, rec); + } + }, + runNowHandler: (button, _event, rec) => { + Ext.Msg.show({ + title: gettext('Confirm'), + icon: Ext.Msg.QUESTION, + msg: gettext('Start the selected backup job now?'), + buttons: Ext.Msg.YESNO, + callback: function (btn) { + if (btn !== 'yes') { + return; + } + + let params = Ext.clone(rec.data); + let paramNodes = params.nodes?.split(',') ?? false; + + delete params.comment; + delete params.enabled; + delete params.id; + delete params.nodes; + delete params.schedule; + delete params.type; + delete params['next-run']; + delete params['repeat-missed']; + + let errors = []; + let allNodes = PVE.data.ResourceStore.getNodes(); + + let nodes = allNodes + .filter((n) => !paramNodes || paramNodes.includes(n.node)) + .filter((n) => { + if (n.status === 'online') { + return true; + } + + errors.push(`${n.node}: ${gettext('Node is offline')}`); + return false; + }) + .map((n) => n.node); + + Ext.Msg.show({ + title: gettext('Please wait...'), + closable: false, + progress: true, + progressText: '0/' + nodes.length, + }); + + let started = 0; + + let postRequest = function () { + started++; + + Ext.Msg.updateProgress( + started / nodes.length, + `${started}/${nodes.length}`, + ); + + if (started === nodes.length) { + Ext.Msg.hide(); + if (errors.length > 0) { + Ext.Msg.alert( + 'Error', + `${gettext('Some errors occurred:')}
${errors.join('
')}`, + ); + } + } + }; + + nodes.forEach((node) => { + Proxmox.Utils.API2Request({ + url: `/nodes/${node}/host-backup`, + method: 'POST', + params, + failure: (response, _opts) => { + errors.push(`${node}: ${response.htmlStatus}`); + postRequest(); + }, + success: postRequest, + }); + }); + }, + }); + }, + dblclickHandler: (element, rec) => { + Ext.create('PVE.dc.AddHostBackupJob', { + autoShow: true, + jobid: rec.data.id, + listeners: { close: () => element.up('grid').getStore().load() }, + }); + }, + }, + + initComponent: function () { + let me = this; + me.callParent(); + Proxmox.Utils.monStoreErrors(me.view, me.store, true); + me.store.load(); + }, +}); -- 2.47.3