From: Stoiko Ivanov <s.ivanov@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [pmg-devel] [PATCH pmg-gui v3 3/3] add PBSConfig tab to Backup menu
Date: Mon, 16 Nov 2020 12:01:18 +0100 [thread overview]
Message-ID: <20201116110118.7483-12-s.ivanov@proxmox.com> (raw)
In-Reply-To: <20201116110118.7483-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 | 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
next prev parent reply other threads:[~2020-11-16 11:02 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-11-16 11:01 [pmg-devel] [PATCH pve-common/pmg-api/pmg-gui v3] add initial PBS integration Stoiko Ivanov
2020-11-16 11:01 ` [pmg-devel] [PATCH pve-common v3 1/1] add PBSClient module Stoiko Ivanov
2020-11-17 8:49 ` [pmg-devel] applied: " Thomas Lamprecht
2020-11-16 11:01 ` [pmg-devel] [PATCH pmg-api v3 1/7] debian: drop duplicate ', ' in dependencies Stoiko Ivanov
2020-11-16 11:01 ` [pmg-devel] [PATCH pmg-api v3 2/7] add initial SectionConfig for PBS Stoiko Ivanov
2020-11-16 11:01 ` [pmg-devel] [PATCH pmg-api v3 3/7] Add API2 module for PBS configuration Stoiko Ivanov
2020-11-16 11:01 ` [pmg-devel] [PATCH pmg-api v3 4/7] Add API2 module for per-node backups to PBS Stoiko Ivanov
2020-11-16 11:01 ` [pmg-devel] [PATCH pmg-api v3 5/7] pbs-integration: add CLI calls to pmgbackup Stoiko Ivanov
2020-11-16 11:01 ` [pmg-devel] [PATCH pmg-api v3 6/7] add scheduled backup to PBS remotes Stoiko Ivanov
2020-11-16 11:01 ` [pmg-devel] [PATCH pmg-api v3 7/7] add /etc/pmg/pbs to cluster-sync Stoiko Ivanov
2020-11-16 11:01 ` [pmg-devel] [PATCH pmg-gui v3 1/3] Make Backup/Restore panel a menuentry Stoiko Ivanov
2020-11-16 11:01 ` [pmg-devel] [PATCH pmg-gui v3 2/3] refactor RestoreWindow for PBS Stoiko Ivanov
2020-11-16 11:01 ` Stoiko Ivanov [this message]
2020-11-17 17:22 ` [pmg-devel] applied-series: [PATCH pve-common/pmg-api/pmg-gui v3] add initial PBS integration Thomas Lamprecht
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=20201116110118.7483-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal