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 B8A1162B9A for ; Tue, 24 Nov 2020 14:00:59 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id AB295C439 for ; Tue, 24 Nov 2020 14:00:59 +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 C26B3C430 for ; Tue, 24 Nov 2020 14:00:58 +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 82E82424C0 for ; Tue, 24 Nov 2020 14:00:58 +0100 (CET) From: Fabian Ebner To: pve-devel@lists.proxmox.com Date: Tue, 24 Nov 2020 14:00:53 +0100 Message-Id: <20201124130053.10494-1-f.ebner@proxmox.com> X-Mailer: git-send-email 2.20.1 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.009 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.storage] Subject: [pve-devel] [PATCH v5 manager] ui: storage backup view: add prune window X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 24 Nov 2020 13:00:59 -0000 adapted from PBS. Main differences are: * API has GET/DELETE distinction instead of 'dry-run' * API expects a single property string for the prune options Signed-off-by: Fabian Ebner --- Needs a dependency bump for proxmox-widget-toolkit. Changes from v4: * Switch to widget toolkit's prune keep fields. * Don't load values from the storage initially. While it could be done, doing a manual prune doesn't need to have to do anything at all with the configuration on the storage. Problem is also that a clear trigger would reset to that value instead of clearing, which obviously makes sense when editing the storage configuration, but not really for doing a one-shot prune operation. * Use "renamed" as a reason instead of "strange name" for renamed backups www/manager6/Makefile | 1 + www/manager6/storage/BackupView.js | 51 ++++++ www/manager6/window/Prune.js | 257 +++++++++++++++++++++++++++++ 3 files changed, 309 insertions(+) create mode 100644 www/manager6/window/Prune.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 9e6e56ef..85f90ecd 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -97,6 +97,7 @@ JSSRC= \ window/LoginWindow.js \ window/Migrate.js \ window/NotesEdit.js \ + window/Prune.js \ window/Restore.js \ window/SafeDestroy.js \ window/Settings.js \ diff --git a/www/manager6/storage/BackupView.js b/www/manager6/storage/BackupView.js index 8c1e2ed6..632a1d36 100644 --- a/www/manager6/storage/BackupView.js +++ b/www/manager6/storage/BackupView.js @@ -24,6 +24,56 @@ Ext.define('PVE.storage.BackupView', { me.store.load(); }; + let pruneButton = Ext.create('Proxmox.button.Button', { + text: gettext('Prune group'), + disabled: true, + selModel: sm, + setBackupGroup: function(backup) { + if (backup) { + let name = backup.text; + let vmid = backup.vmid; + let format = backup.format; + + let vmtype; + if (name.startsWith('vzdump-lxc-') || format === "pbs-ct") { + vmtype = 'lxc'; + } else if (name.startsWith('vzdump-qemu-') || format === "pbs-vm") { + vmtype = 'qemu'; + } + + if (vmid && vmtype) { + this.setText(gettext('Prune group') + ` ${vmtype}/${vmid}`); + this.vmid = vmid; + this.vmtype = vmtype; + this.setDisabled(false); + return; + } + } + this.setText(gettext('Prune group')); + this.vmid = null; + this.vmtype = null; + this.setDisabled(true); + }, + handler: function(b, e, rec) { + let win = Ext.create('PVE.window.Prune', { + nodename: nodename, + storage: storage, + backup_id: this.vmid, + backup_type: this.vmtype, + }); + win.show(); + win.on('destroy', reload); + }, + }); + + me.on('selectionchange', function(model, srecords, eOpts) { + if (srecords.length === 1) { + pruneButton.setBackupGroup(srecords[0].data); + } else { + pruneButton.setBackupGroup(null); + } + }); + me.tbar = [ { xtype: 'proxmoxButton', @@ -64,6 +114,7 @@ Ext.define('PVE.storage.BackupView', { win.show(); } }, + pruneButton, ]; me.callParent(); diff --git a/www/manager6/window/Prune.js b/www/manager6/window/Prune.js new file mode 100644 index 00000000..f503773d --- /dev/null +++ b/www/manager6/window/Prune.js @@ -0,0 +1,257 @@ +Ext.define('pve-prune-list', { + extend: 'Ext.data.Model', + fields: [ + 'type', + 'vmid', + { + name: 'ctime', + type: 'date', + dateFormat: 'timestamp', + }, + ], +}); + +Ext.define('PVE.PruneInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pvePruneInputPanel', + mixins: ['Proxmox.Mixin.CBind'], + + onGetValues: function(values) { + let me = this; + + // the API expects a single prune-backups property string + let pruneBackups = PVE.Parser.printPropertyString(values); + values = { + 'prune-backups': pruneBackups, + 'type': me.backup_type, + 'vmid': me.backup_id, + }; + + return values; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + if (!view.url) { + throw "no url specified"; + } + if (!view.backup_type) { + throw "no backup_type specified"; + } + if (!view.backup_id) { + throw "no backup_id specified"; + } + + this.reload(); // initial load + }, + + reload: function() { + let view = this.getView(); + + // helper to allow showing why a backup is kept + let addKeepReasons = function(backups, params) { + const rules = [ + 'keep-last', + 'keep-hourly', + 'keep-daily', + 'keep-weekly', + 'keep-monthly', + 'keep-yearly', + 'keep-all', // when all keep options are not set + ]; + let counter = {}; + + backups.sort(function(a, b) { + return a.ctime < b.ctime; + }); + + let ruleIndex = -1; + let nextRule = function() { + let rule; + do { + ruleIndex++; + rule = rules[ruleIndex]; + } while (!params[rule] && rule !== 'keep-all'); + counter[rule] = 0; + return rule; + }; + + let rule = nextRule(); + for (let backup of backups) { + if (backup.mark === 'keep') { + counter[rule]++; + if (rule !== 'keep-all') { + backup.keepReason = rule + ': ' + counter[rule]; + if (counter[rule] >= params[rule]) { + rule = nextRule(); + } + } else { + backup.keepReason = rule; + } + } + } + }; + + let params = view.getValues(); + let keepParams = PVE.Parser.parsePropertyString(params["prune-backups"]); + + Proxmox.Utils.API2Request({ + url: view.url, + method: "GET", + params: params, + callback: function() { + // for easy breakpoint setting + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var data = response.result.data; + addKeepReasons(data, keepParams); + view.pruneStore.setData(data); + }, + }); + }, + + control: { + field: { change: 'reload' }, + }, + }, + + column1: [ + { + xtype: 'pmxPruneKeepField', + name: 'keep-last', + fieldLabel: gettext('keep-last'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-hourly', + fieldLabel: gettext('keep-hourly'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-daily', + fieldLabel: gettext('keep-daily'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-weekly', + fieldLabel: gettext('keep-weekly'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-monthly', + fieldLabel: gettext('keep-monthly'), + }, + { + xtype: 'pmxPruneKeepField', + name: 'keep-yearly', + fieldLabel: gettext('keep-yearly'), + }, + ], + + initComponent: function() { + var me = this; + + me.pruneStore = Ext.create('Ext.data.Store', { + model: 'pve-prune-list', + sorters: { property: 'ctime', direction: 'DESC' }, + }); + + me.column2 = [ + { + xtype: 'grid', + height: 200, + store: me.pruneStore, + columns: [ + { + header: gettext('Backup Time'), + sortable: true, + dataIndex: 'ctime', + renderer: function(value, metaData, record) { + let text = Ext.Date.format(value, 'Y-m-d H:i:s'); + if (record.data.mark === 'remove') { + return '
'+ text +'
'; + } else { + return text; + } + }, + flex: 1, + }, + { + text: 'Keep (reason)', + dataIndex: 'mark', + renderer: function(value, metaData, record) { + if (record.data.mark === 'keep') { + return 'true (' + record.data.keepReason + ')'; + } else if (record.data.mark === 'protected') { + return 'true (renamed)'; + } else { + return 'false'; + } + }, + flex: 1, + }, + ], + }, + ]; + + me.callParent(); + }, +}); + +Ext.define('PVE.window.Prune', { + extend: 'Proxmox.window.Edit', + + method: 'DELETE', + submitText: gettext("Prune"), + + fieldDefaults: { labelWidth: 130 }, + + isCreate: true, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename specified"; + } + if (!me.storage) { + throw "no storage specified"; + } + if (!me.backup_type) { + throw "no backup_type specified"; + } + if (me.backup_type !== 'qemu' && me.backup_type !== 'lxc') { + throw "unknown backup type: " + me.backup_type; + } + if (!me.backup_id) { + throw "no backup_id specified"; + } + + let title = Ext.String.format( + gettext("Prune Backups for '{0}' on Storage '{1}'"), + me.backup_type + '/' + me.backup_id, + me.storage, + ); + + Ext.apply(me, { + url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups", + title: title, + items: [ + { + xtype: 'pvePruneInputPanel', + url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups", + backup_type: me.backup_type, + backup_id: me.backup_id, + storage: me.storage, + }, + ], + }); + + me.callParent(); + }, +}); -- 2.20.1