From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: 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 9576069F43 for ; Mon, 16 Nov 2020 12:02:08 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 5DC5E2DB6A for ; Mon, 16 Nov 2020 12:01:38 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (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 id BB75A2DAFD for ; Mon, 16 Nov 2020 12:01:33 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 8461843662 for ; Mon, 16 Nov 2020 12:01:33 +0100 (CET) From: Stoiko Ivanov To: pmg-devel@lists.proxmox.com Date: Mon, 16 Nov 2020 12:01:18 +0100 Message-Id: <20201116110118.7483-12-s.ivanov@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201116110118.7483-1-s.ivanov@proxmox.com> References: <20201116110118.7483-1-s.ivanov@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.090 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [me.name] Subject: [pmg-devel] [PATCH pmg-gui v3 3/3] add PBSConfig tab to Backup menu X-BeenThere: pmg-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Mail Gateway development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 16 Nov 2020 11:02:08 -0000 The PBSConfig panel enables creation/editing/deletion of PBS instances. Each instance can lists its snapshots and each snapshot can be restored Inspired by the LDAPConfig panel and PBSEdit from pve-manager. Signed-off-by: Stoiko Ivanov --- js/BackupConfiguration.js | 5 + js/BackupRestore.js | 9 + js/Makefile | 1 + js/PBSConfig.js | 678 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 693 insertions(+) create mode 100644 js/PBSConfig.js diff --git a/js/BackupConfiguration.js b/js/BackupConfiguration.js index 35b50a4..e21771f 100644 --- a/js/BackupConfiguration.js +++ b/js/BackupConfiguration.js @@ -13,6 +13,11 @@ Ext.define('PMG.BackupConfiguration', { title: gettext('Local Backup/Restore'), xtype: 'pmgBackupRestore', }, + { + itemId: 'proxmoxbackupserver', + title: 'Proxmox Backup Server', + xtype: 'pmgPBSConfig', + }, ], }); diff --git a/js/BackupRestore.js b/js/BackupRestore.js index 2c90f2e..2b9ce53 100644 --- a/js/BackupRestore.js +++ b/js/BackupRestore.js @@ -58,6 +58,15 @@ Ext.define('PMG.RestoreWindow', { let initurl = "/nodes/" + Proxmox.NodeName; if (me.filename) { me.url = initurl + "/backup/" + encodeURIComponent(me.filename); + } else if (me.backup_time) { + me.items.push( + { + xtype: 'hiddenfield', + name: 'backup-time', + value: me.backup_time, + }, + ); + me.url = initurl + "/pbs/" + me.name + '/restore'; } else { throw "neither filename nor snapshot given"; } diff --git a/js/Makefile b/js/Makefile index 47eabb7..42eaeb0 100644 --- a/js/Makefile +++ b/js/Makefile @@ -37,6 +37,7 @@ JSSRC= \ Subscription.js \ BackupConfiguration.js \ BackupRestore.js \ + PBSConfig.js \ SystemConfiguration.js \ MailProxyRelaying.js \ MailProxyPorts.js \ diff --git a/js/PBSConfig.js b/js/PBSConfig.js new file mode 100644 index 0000000..9a14d6d --- /dev/null +++ b/js/PBSConfig.js @@ -0,0 +1,678 @@ +Ext.define('Proxmox.form.PBSEncryptionCheckbox', { + extend: 'Ext.form.field.Checkbox', + xtype: 'pbsEncryptionCheckbox', + + inputValue: true, + + viewModel: { + data: { + value: null, + originalValue: null, + }, + formulas: { + blabel: (get) => { + let v = get('value'); + let original = get('originalValue'); + if (!get('isCreate') && original) { + if (!v) { + return gettext('Warning: Existing encryption key will be deleted!'); + } + return gettext('Active'); + } else { + return gettext('Auto-generate a client encryption key, saved privately in /etc/pmg'); + } + }, + }, + }, + + bind: { + value: '{value}', + boxLabel: '{blabel}', + }, + resetOriginalValue: function() { + let me = this; + let vm = me.getViewModel(); + vm.set('originalValue', me.value); + + me.callParent(arguments); + }, + + getSubmitData: function() { + let me = this; + let val = me.getSubmitValue(); + if (!me.isCreate) { + if (val === null) { + return { 'delete': 'encryption-key' }; + } else if (val && !!val !== !!me.originalValue) { + return { 'encryption-key': 'autogen' }; + } + } else if (val) { + return { 'encryption-key': 'autogen' }; + } + return null; + }, + + initComponent: function() { + let me = this; + me.callParent(); + + let vm = me.getViewModel(); + vm.set('isCreate', me.isCreate); + }, +}); + +Ext.define('PMG.PBSInputPanel', { + extend: 'Ext.tab.Panel', + xtype: 'pmgPBSInputPanel', + + bodyPadding: 10, + remoteId: undefined, + + initComponent: function() { + let me = this; + + me.items = [ + { + title: gettext('Backup Server'), + xtype: 'inputpanel', + reference: 'remoteeditpanel', + onGetValues: function(values) { + values.disable = values.enable ? 0 : 1; + delete values.enable; + + return values; + }, + + column1: [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'remote', + value: me.isCreate ? null : undefined, + fieldLabel: gettext('ID'), + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + name: 'server', + value: me.isCreate ? null : undefined, + vtype: 'DnsOrIp', + fieldLabel: gettext('Server'), + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + name: 'datastore', + value: me.isCreate ? null : undefined, + fieldLabel: 'Datastore', + allowBlank: false, + }, + ], + column2: [ + { + xtype: 'proxmoxtextfield', + name: 'username', + value: me.isCreate ? null : undefined, + emptyText: gettext('Example') + ': admin@pbs', + fieldLabel: gettext('Username'), + regex: /\S+@\w+/, + regexText: gettext('Example') + ': admin@pbs', + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + inputType: 'password', + name: 'password', + value: me.isCreate ? null : undefined, + emptyText: me.isCreate ? gettext('None') : '********', + fieldLabel: gettext('Password'), + allowBlank: true, + }, + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Enable'), + }, + ], + columnB: [ + { + xtype: 'proxmoxtextfield', + name: 'fingerprint', + value: me.isCreate ? null : undefined, + fieldLabel: gettext('Fingerprint'), + emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'), + regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/, + regexText: gettext('Example') + ': AB:CD:EF:...', + allowBlank: true, + }, + { + xtype: 'pbsEncryptionCheckbox', + name: 'encryption-key', + isCreate: me.isCreate, + fieldLabel: gettext('Encryption Key'), + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: `Proxmox Backup Server is currently in beta.`, + }, + ], + }, + { + title: gettext('Prune Options'), + xtype: 'inputpanel', + reference: 'prunepanel', + column1: [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Keep Last'), + name: 'keep-last', + cbind: { + deleteEmpty: '{!isCreate}', + }, + minValue: 1, + allowBlank: true, + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Keep Daily'), + name: 'keep-daily', + cbind: { + deleteEmpty: '{!isCreate}', + }, + minValue: 1, + allowBlank: true, + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Keep Monthly'), + name: 'keep-monthly', + cbind: { + deleteEmpty: '{!isCreate}', + }, + minValue: 1, + allowBlank: true, + }, + ], + column2: [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Keep Hourly'), + name: 'keep-hourly', + cbind: { + deleteEmpty: '{!isCreate}', + }, + minValue: 1, + allowBlank: true, + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Keep Weekly'), + name: 'keep-weekly', + cbind: { + deleteEmpty: '{!isCreate}', + }, + minValue: 1, + allowBlank: true, + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Keep Yearly'), + name: 'keep-yearly', + cbind: { + deleteEmpty: '{!isCreate}', + }, + minValue: 1, + allowBlank: true, + }, + ], + }, + ]; + + me.callParent(); + }, + +}); + +Ext.define('PMG.PBSEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pmgPBSEdit', + + subject: 'Proxmox Backup Server', + isAdd: true, + + bodyPadding: 0, + + initComponent: function() { + let me = this; + + me.isCreate = !me.remoteId; + + if (me.isCreate) { + me.url = '/api2/extjs/config/pbs'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/config/pbs/' + me.remoteId; + me.method = 'PUT'; + } + + let ipanel = Ext.create('PMG.PBSInputPanel', { + isCreate: me.isCreate, + remoteId: me.remoteId, + }); + + me.items = [ipanel]; + + me.fieldDefaults = { + labelWidth: 150, + }; + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + let values = response.result.data; + + values.enable = values.disable ? 0 : 1; + me.down('inputpanel[reference=remoteeditpanel]').setValues(values); + me.down('inputpanel[reference=prunepanel]').setValues(values); + }, + }); + } + }, +}); + +Ext.define('PMG.PBSScheduleEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pmgPBSScheduleEdit', + + isAdd: true, + method: 'POST', + subject: gettext('Scheduled Backup'), + autoLoad: true, + items: [ + { + xtype: 'proxmoxKVComboBox', + name: 'schedule', + fieldLabel: gettext('Schedule'), + comboItems: [ + ['daily', 'daily'], + ['hourly', 'hourly'], + ['weekly', 'weekly'], + ['monthly', 'monthly'], + ], + editable: true, + emptyText: 'Systemd Calender Event', + }, + { + xtype: 'proxmoxKVComboBox', + name: 'delay', + fieldLabel: gettext('Random Delay'), + comboItems: [ + ['0s', 'no delay'], + ['15 minutes', '15 Minutes'], + ['6 hours', '6 hours'], + ], + editable: true, + emptyText: 'Systemd TimeSpan', + }, + ], + initComponent: function() { + let me = this; + + me.url = '/nodes/' + Proxmox.NodeName + '/pbs/' + me.remote + '/timer'; + me.callParent(); + }, +}); + +Ext.define('PMG.PBSConfig', { + extend: 'Ext.panel.Panel', + xtype: 'pmgPBSConfig', + + controller: { + xclass: 'Ext.app.ViewController', + + callRestore: function(grid, record) { + let name = this.getViewModel().get('name'); + Ext.create('PMG.RestoreWindow', { + name: name, + backup_time: record.data.time, + }).show(); + }, + + restoreSnapshot: function(button) { + let me = this; + let view = me.lookup('pbsremotegrid'); + let record = view.getSelection()[0]; + me.callRestore(view, record); + }, + + runBackup: function(button) { + let me = this; + let view = me.lookup('pbsremotegrid'); + let name = me.getViewModel().get('name'); + Proxmox.Utils.API2Request({ + url: "/nodes/" + Proxmox.NodeName + "/pbs/" + name + "/backup", + method: 'POST', + waitMsgTarget: view, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + let upid = response.result.data; + + let win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + }); + win.show(); + me.mon(win, 'close', function() { view.getStore().load(); }); + }, + }); + }, + + reload: function(grid) { + let me = this; + let selection = grid.getSelection(); + me.showInfo(grid, selection); + }, + + showInfo: function(grid, selected) { + let me = this; + let viewModel = me.getViewModel(); + if (selected[0]) { + let name = selected[0].data.remote; + viewModel.set('selected', true); + viewModel.set('name', name); + + // set grid stores and load them + let remstore = me.lookup('pbsremotegrid').getStore(); + remstore.getProxy().setUrl('/api2/json/nodes/' + Proxmox.NodeName + '/pbs/' + name + '/snapshots'); + remstore.load(); + } else { + viewModel.set('selected', false); + } + }, + reloadSnapshots: function() { + let me = this; + let grid = me.lookup('grid'); + let selection = grid.getSelection(); + me.showInfo(grid, selection); + }, + init: function(view) { + let me = this; + me.lookup('grid').relayEvents(view, ['activate']); + let pbsremotegrid = me.lookup('pbsremotegrid'); + + Proxmox.Utils.monStoreErrors(pbsremotegrid, pbsremotegrid.getStore(), true); + }, + + control: { + 'grid[reference=grid]': { + selectionchange: 'showInfo', + load: 'reload', + }, + 'grid[reference=pbsremotegrid]': { + itemdblclick: 'restoreSnapshot', + }, + }, + }, + + viewModel: { + data: { + name: '', + selected: false, + }, + }, + + layout: 'border', + + items: [ + { + region: 'center', + reference: 'grid', + xtype: 'pmgPBSConfigGrid', + border: false, + }, + { + xtype: 'grid', + region: 'south', + reference: 'pbsremotegrid', + hidden: true, + height: '70%', + border: false, + split: true, + emptyText: gettext('No backups on remote'), + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Backup'), + handler: 'runBackup', + selModel: false, + }, + { + xtype: 'proxmoxButton', + text: gettext('Restore'), + handler: 'restoreSnapshot', + disabled: true, + }, + { + xtype: 'proxmoxStdRemoveButton', + text: gettext('Forget Snapshot'), + disabled: true, + getUrl: function(rec) { + let me = this; + let remote = me.lookupViewModel().get('name'); + return '/nodes/' + Proxmox.NodeName + '/pbs/' + remote +'/snapshots/'+ rec.data.time; + }, + confirmMsg: function(rec) { + let me = this; + let time = rec.data.time; + return Ext.String.format(gettext('Are you sure you want to forget snapshot {0}'), `'${time}'`); + }, + callback: 'reloadSnapshots', + }, + ], + store: { + fields: ['time', 'size', 'ctime', 'encrypted'], + proxy: { type: 'proxmox' }, + sorters: [ + { + property: 'time', + direction: 'DESC', + }, + ], + }, + bind: { + title: Ext.String.format(gettext("Backup snapshots on '{0}'"), '{name}'), + hidden: '{!selected}', + }, + columns: [ + { + text: 'Time', + dataIndex: 'time', + flex: 1, + }, + { + text: 'Size', + dataIndex: 'size', + flex: 1, + }, + { + text: 'Encrypted', + dataIndex: 'encrypted', + renderer: Proxmox.Utils.format_boolean, + flex: 1, + }, + ], + }, + ], + +}); + +Ext.define('pmg-pbs-config', { + extend: 'Ext.data.Model', + fields: ['remote', 'server', 'datastore', 'username', 'disabled'], + proxy: { + type: 'proxmox', + url: '/api2/json/config/pbs', + }, + idProperty: 'remote', +}); + +Ext.define('PMG.PBSConfigGrid', { + extend: 'Ext.grid.GridPanel', + xtype: 'pmgPBSConfigGrid', + + controller: { + xclass: 'Ext.app.ViewController', + + run_editor: function() { + let me = this; + let view = me.getView(); + let rec = view.getSelection()[0]; + if (!rec) { + return; + } + + let win = Ext.createWidget('pmgPBSEdit', { + remoteId: rec.data.remote, + }); + win.on('destroy', me.reload, me); + win.load(); + win.show(); + }, + + newRemote: function() { + let me = this; + let win = Ext.createWidget('pmgPBSEdit', {}); + win.on('destroy', me.reload, me); + win.show(); + }, + + + reload: function() { + let me = this; + let view = me.getView(); + view.getStore().load(); + view.fireEvent('load', view); + }, + + createSchedule: function() { + let me = this; + let view = me.getView(); + let rec = view.getSelection()[0]; + let remotename = rec.data.remote; + let win = Ext.createWidget('pmgPBSScheduleEdit', { + remote: remotename, + }); + win.on('destroy', me.reload, me); + win.show(); + }, + + init: function(view) { + let me = this; + Proxmox.Utils.monStoreErrors(view, view.getStore(), true); + }, + + }, + + store: { + model: 'pmg-pbs-config', + sorters: [{ + property: 'remote', + order: 'DESC', + }], + }, + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + handler: 'run_editor', + }, + { + text: gettext('Create'), + handler: 'newRemote', + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: '/config/pbs', + callback: 'reload', + }, + { + xtype: 'proxmoxButton', + text: gettext('Schedule'), + enableFn: function(rec) { + return !rec.data.disable; + }, + disabled: true, + handler: 'createSchedule', + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: '/nodes/' + Proxmox.NodeName + '/pbs/', + callback: 'reload', + text: gettext('Remove Schedule'), + confirmMsg: function(rec) { + let me = this; + let name = rec.getId(); + return Ext.String.format(gettext('Are you sure you want to remove the schedule for {0}'), `'${name}'`); + }, + getUrl: function(rec) { + let me = this; + return me.baseurl + '/' + rec.getId() + '/timer'; + }, + }, + ], + + listeners: { + itemdblclick: 'run_editor', + activate: 'reload', + }, + + columns: [ + { + header: gettext('Backup Server name'), + sortable: true, + dataIndex: 'remote', + flex: 2, + }, + { + header: gettext('Server'), + sortable: true, + dataIndex: 'server', + flex: 2, + }, + { + header: gettext('Datastore'), + sortable: true, + dataIndex: 'datastore', + flex: 1, + }, + { + header: gettext('User ID'), + sortable: true, + dataIndex: 'username', + flex: 1, + }, + { + header: gettext('Encryption'), + width: 80, + sortable: true, + dataIndex: 'encryption-key', + renderer: Proxmox.Utils.format_boolean, + }, + { + header: gettext('Enabled'), + width: 80, + sortable: true, + dataIndex: 'disable', + renderer: Proxmox.Utils.format_neg_boolean, + }, + ], + +}); + -- 2.20.1