public inbox for pmg-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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





  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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal