all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Stoiko Ivanov <s.ivanov@proxmox.com>
To: pmg-devel@pve.proxmox.com
Subject: [pmg-devel] [PATCH pmg-gui 3/3] add PBSConfig tab to Backup menu
Date: Wed, 28 Oct 2020 19:54:32 +0100	[thread overview]
Message-ID: <20201028185432.23067-17-s.ivanov@proxmox.com> (raw)
In-Reply-To: <20201028185432.23067-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





  parent reply	other threads:[~2020-10-28 18:54 UTC|newest]

Thread overview: 26+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files Stoiko Ivanov
2020-10-30  8:26   ` [pmg-devel] applied: " Dietmar Maurer
2020-11-10  8:34   ` [pmg-devel] " Thomas Lamprecht
2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 2/2] add helper module for handling PBS Integration Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 01/11] drop left-over commented out code Stoiko Ivanov
2020-10-30  5:58   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 02/11] Backup: split backup creation and creating tar Stoiko Ivanov
2020-10-30  6:20   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 03/11] Restore: optionally restore from directory Stoiko Ivanov
2020-10-30  6:26   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 04/11] Backup: push restore options to PMG::Backup Stoiko Ivanov
2020-10-30  6:32   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 05/11] debian: add dependency on proxmox-backup-client Stoiko Ivanov
2020-10-30  6:33   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 06/11] add initial SectionConfig for pbs Stoiko Ivanov
2020-10-30  6:38   ` Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 07/11] Add API2 module for PBS configuration Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 08/11] Add API2 module for per-node backups to PBS Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 09/11] pbs-integration: add CLI calls to pmgbackup Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 10/11] add scheduled backup to PBS remotes Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 11/11] add daily timer for pruning configured remotes Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 1/3] Make Backup/Restore panel a menuentry Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 2/3] refactor RestoreWindow for PBS Stoiko Ivanov
2020-10-28 18:54 ` Stoiko Ivanov [this message]
2020-10-30  5:47 ` [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Dietmar Maurer

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=20201028185432.23067-17-s.ivanov@proxmox.com \
    --to=s.ivanov@proxmox.com \
    --cc=pmg-devel@pve.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