From: Stoiko Ivanov <s.ivanov@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [pmg-devel] [PATCH pmg-gui v2 3/3] add PBSConfig tab to Backup menu
Date: Mon, 2 Nov 2020 19:45:38 +0100 [thread overview]
Message-ID: <20201102184538.17127-12-s.ivanov@proxmox.com> (raw)
In-Reply-To: <20201102184538.17127-1-s.ivanov@proxmox.com>
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 <s.ivanov@proxmox.com>
---
js/BackupConfiguration.js | 5 +
js/BackupRestore.js | 9 +
js/Makefile | 1 +
js/PBSConfig.js | 680 ++++++++++++++++++++++++++++++++++++++
4 files changed, 695 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 996f128..9981d42 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 a40f11f..bc14487 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..abec9d7
--- /dev/null
+++ b/js/PBSConfig.js
@@ -0,0 +1,680 @@
+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) {
+ let me = this;
+
+ 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
prev parent reply other threads:[~2020-11-02 18:46 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-11-02 18:45 [pmg-devel] [PATCH v2 pve-common/api/gui] add initial PBS integration Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pve-common v2 1/1] add PBSClient module Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pmg-api v2 1/7] debian: drop duplicate ', ' in dependencies Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pmg-api v2 2/7] add initial SectionConfig for PBS Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pmg-api v2 3/7] Add API2 module for PBS configuration Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pmg-api v2 4/7] Add API2 module for per-node backups to PBS Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pmg-api v2 5/7] pbs-integration: add CLI calls to pmgbackup Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pmg-api v2 6/7] add scheduled backup to PBS remotes Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pmg-api v2 7/7] add /etc/pmg/pbs to cluster-sync Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pmg-gui v2 1/3] Make Backup/Restore panel a menuentry Stoiko Ivanov
2020-11-02 18:45 ` [pmg-devel] [PATCH pmg-gui v2 2/3] refactor RestoreWindow for PBS Stoiko Ivanov
2020-11-02 18:45 ` Stoiko Ivanov [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20201102184538.17127-12-s.ivanov@proxmox.com \
--to=s.ivanov@proxmox.com \
--cc=pmg-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox