* [PATCH 0/2] RFC: manager: add direct VM backup export/import support
@ 2026-06-26 16:43 Mauro de Pascale
2026-06-26 16:43 ` [PATCH 1/2] manager: add API endpoint for streamed VM backup export Mauro de Pascale
2026-06-26 16:43 ` [PATCH 2/2] manager: add GUI support for backup export and import Mauro de Pascale
0 siblings, 2 replies; 3+ messages in thread
From: Mauro de Pascale @ 2026-06-26 16:43 UTC (permalink / raw)
To: pve-devel@lists.proxmox.com; +Cc: Mauro de Pascale
From: Mauro de Pascale <mauro.depascale.work@outlook.it>
This RFC proposes a minimal implementation for direct VM backup export and
import workflows, avoiding the need for an intermediate storage location.
The goal is to explore a possible extension of Proxmox VE towards a workflow
already available in other virtualization platforms, where VM archives can be
transferred directly between the client and the host.
This series implements the manager side of the feature:
* add an API endpoint to stream vzdump output directly to the client;
* extend the web interface to export backups and upload VMA archives for
restore.
The implementation intentionally keeps the changes small and reuses existing
Proxmox components:
* vzdump is used as the existing backup generator;
* existing upload and restore mechanisms are reused for import;
* no new transfer protocol or storage layer is introduced.
The corresponding pve-storage change is required for the complete import
workflow, as it extends the existing upload path to handle VMA restore
operations.
This is an RFC and is intended to gather feedback about the approach rather
than propose a final feature implementation. Possible future improvements
could include richer progress reporting, improved error handling, additional
transfer workflows, or a dedicated API if this direction is considered useful.
Depends on:
- pve-storage RFC: storage: allow VM restore from uploaded VMA archives
Mauro de Pascale (2):
manager: add API endpoint for streamed VM backup export
manager: add GUI support for backup export and import
PVE/API2/VZDump.pm | 65 ++++++++++++++++++++++++++
www/manager6/Utils.js | 16 +++----
www/manager6/grid/BackupView.js | 24 +++++++++-
www/manager6/storage/BackupView.js | 29 +++++++++++-
www/manager6/window/Backup.js | 33 ++++++++++++-
www/manager6/window/UploadToStorage.js | 65 ++++++++++++++++++++++++--
6 files changed, 213 insertions(+), 19 deletions(-)
--
2.47.3
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 1/2] manager: add API endpoint for streamed VM backup export
2026-06-26 16:43 [PATCH 0/2] RFC: manager: add direct VM backup export/import support Mauro de Pascale
@ 2026-06-26 16:43 ` Mauro de Pascale
2026-06-26 16:43 ` [PATCH 2/2] manager: add GUI support for backup export and import Mauro de Pascale
1 sibling, 0 replies; 3+ messages in thread
From: Mauro de Pascale @ 2026-06-26 16:43 UTC (permalink / raw)
To: pve-devel@lists.proxmox.com; +Cc: Mauro de Pascale
From: Mauro de Pascale <mauro.depascale.work@outlook.it>
Add an API endpoint that exposes vzdump output as a streamed download,
allowing clients to export VM backups directly without storing them on
the server first.
---
PVE/API2/VZDump.pm | 65 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
diff --git a/PVE/API2/VZDump.pm b/PVE/API2/VZDump.pm
index a8f21eba..84f42352 100644
--- a/PVE/API2/VZDump.pm
+++ b/PVE/API2/VZDump.pm
@@ -9,11 +9,15 @@ use PVE::Exception qw(raise_param_exc);
use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
use PVE::RPCEnvironment;
+use PVE::SafeSyslog;
use PVE::Storage;
use PVE::Tools qw(extract_param);
use PVE::VZDump::Common;
use PVE::VZDump;
+use IPC::Open3;
+use Symbol qw(gensym);
+
use PVE::API2::Backup;
use PVE::API2Tools;
@@ -311,4 +315,65 @@ __PACKAGE__->register_method({
},
});
+__PACKAGE__->register_method({
+ name => 'export',
+ path => 'export',
+ method => 'GET',
+ description => "Export a VM dump stream",
+ proxyto => 'node',
+ protected => 1,
+ permissions => {
+ check => ['perm', '/vms/{vmid}', ['VM.Backup']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ vmid => get_standard_option('pve-vmid'),
+ compress => {
+ description => "compression algorithm to use.",
+ type => 'string',
+ enum => ['zstd', 'gzip', 'lzo', '0'],
+ optional => 1,
+ default => 'zstd',
+ },
+ },
+ },
+ returns => { type => 'object' },
+ download => 1,
+ code => sub {
+ my ($param) = @_;
+
+ my $vmid = $param->{vmid};
+ my $compress = $param->{compress} // 'zstd';
+
+ my $suffix = $compress eq 'zstd' ? 'zst'
+ : $param->{compress} eq 'gzip' ? 'gz'
+ : $param->{compress} eq 'lzo' ? 'lzo'
+ : 'vma';
+
+ my $filename = "vzdump-qemu-$vmid.vma.$suffix";
+
+ my $cmd = ['/usr/bin/vzdump', $vmid, '--stdout','1','--compress',$compress];
+ my $cmdstr = join(' ', @$cmd);
+ syslog('info', "running export cmd: $cmdstr");
+
+ #my ($fh, $pid) = PVE::Tools::run_command($cmd, pipe => 1);
+ open my $fh, '-|', @$cmd
+ or die "unable to execute backup $!\n";
+
+ binmode($fh);
+
+
+ return {
+ download => {
+ fh => $fh,
+ filename => $filename,
+ stream => 1,
+ 'content-type' => 'application/octet-stream',
+ 'content-disposition' => "attachment; filename=\"$filename\"",
+ }
+ };
+ }});
+
1;
--
2.47.3
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH 2/2] manager: add GUI support for backup export and import
2026-06-26 16:43 [PATCH 0/2] RFC: manager: add direct VM backup export/import support Mauro de Pascale
2026-06-26 16:43 ` [PATCH 1/2] manager: add API endpoint for streamed VM backup export Mauro de Pascale
@ 2026-06-26 16:43 ` Mauro de Pascale
1 sibling, 0 replies; 3+ messages in thread
From: Mauro de Pascale @ 2026-06-26 16:43 UTC (permalink / raw)
To: pve-devel@lists.proxmox.com; +Cc: Mauro de Pascale
From: Mauro de Pascale <mauro.depascale.work@outlook.it>
Expose backup export from the backup dialog and allow uploading VMA
archives for restore directly from the storage view.
---
PVE/API2/VZDump.pm | 2 +-
www/manager6/Utils.js | 16 +++----
www/manager6/grid/BackupView.js | 24 +++++++++-
www/manager6/storage/BackupView.js | 29 +++++++++++-
www/manager6/window/Backup.js | 33 ++++++++++++-
www/manager6/window/UploadToStorage.js | 65 ++++++++++++++++++++++++--
6 files changed, 149 insertions(+), 20 deletions(-)
diff --git a/PVE/API2/VZDump.pm b/PVE/API2/VZDump.pm
index 84f42352..45c316c1 100644
--- a/PVE/API2/VZDump.pm
+++ b/PVE/API2/VZDump.pm
@@ -351,7 +351,7 @@ __PACKAGE__->register_method({
: $param->{compress} eq 'gzip' ? 'gz'
: $param->{compress} eq 'lzo' ? 'lzo'
: 'vma';
-
+
my $filename = "vzdump-qemu-$vmid.vma.$suffix";
my $cmd = ['/usr/bin/vzdump', $vmid, '--stdout','1','--compress',$compress];
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 040b5ae0..412fba0a 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -145,7 +145,7 @@ Ext.define('PVE.Utils', {
bvers = b.toString().split('.');
}
- for (;;) {
+ for (; ;) {
let av = avers.shift();
let bv = bvers.shift();
@@ -1272,8 +1272,7 @@ Ext.define('PVE.Utils', {
calculate_disk_usage: function (data) {
if (
!Ext.isNumeric(data.disk) ||
- data.type === 'qemu' ||
- (data.type === 'lxc' && data.uptime === 0) ||
+ ((data.type === 'qemu' || data.type === 'lxc') && data.uptime === 0) ||
data.maxdisk === 0
) {
return -1;
@@ -1298,8 +1297,7 @@ Ext.define('PVE.Utils', {
if (
!Ext.isNumeric(disk) ||
maxdisk === 0 ||
- type === 'qemu' ||
- (type === 'lxc' && record.data.uptime === 0)
+ ((type === 'qemu' || type === 'lxc') && record.data.uptime === 0)
) {
return '';
}
@@ -1810,9 +1808,6 @@ Ext.define('PVE.Utils', {
qemu_min_version: function (toCheck, minVersion) {
let i;
for (i = 0; i < toCheck.length && i < minVersion.length; i++) {
- if (toCheck[i] > minVersion[i]) {
- return true;
- }
if (toCheck[i] < minVersion[i]) {
return false;
}
@@ -1902,8 +1897,8 @@ Ext.define('PVE.Utils', {
container.mask(
Ext.String.format(
gettext('{0} not installed.') +
- ' ' +
- gettext('Log in as root to install.'),
+ ' ' +
+ gettext('Log in as root to install.'),
'Ceph',
),
['pve-static-mask'],
@@ -2185,6 +2180,7 @@ Ext.define('PVE.Utils', {
hastop: ['HA', gettext('Stop')],
imgcopy: ['', gettext('Copy data')],
imgdel: ['', gettext('Erase data')],
+ importvm: ['VM', gettext('Importing')],
lvmcreate: [gettext('LVM Storage'), gettext('Create')],
lvmremove: ['Volume Group', gettext('Remove')],
lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
diff --git a/www/manager6/grid/BackupView.js b/www/manager6/grid/BackupView.js
index 406143dc..e63c7bdc 100644
--- a/www/manager6/grid/BackupView.js
+++ b/www/manager6/grid/BackupView.js
@@ -253,6 +253,27 @@ Ext.define('PVE.grid.BackupView', {
},
});
+ let import_btn = Ext.create('Proxmox.button.Button', {
+ text: 'Import',
+ iconCls: 'fa fa-upload',
+ tooltip: gettext('Import backup and restore VM from local machine'),
+ handler: function () {
+ Ext.create('PVE.window.UploadToStorage', {
+ nodename: nodename,
+ storage: storagesel.getValue(),
+ requiredPermissions: {
+ check: ['Datastore.Allocate', 'VM.Allocate'],
+ },
+ content: 'import',
+ listeners: {
+ destroy: () => reload(),
+ },
+ autoShow: true,
+ });
+ },
+ });
+
+
Ext.apply(me, {
selModel: sm,
tbar: {
@@ -260,6 +281,8 @@ Ext.define('PVE.grid.BackupView', {
items: [
backup_btn,
'-',
+ import_btn,
+ '-',
restore_btn,
file_restore_btn,
config_btn,
@@ -376,7 +399,6 @@ Ext.define('PVE.grid.BackupView', {
renderer: PVE.Utils.render_backup_encryption,
},
{
- // TRANSLATORS: The state of the verification task
header: gettext('Verify State'),
dataIndex: 'verification',
renderer: PVE.Utils.render_backup_verification,
diff --git a/www/manager6/storage/BackupView.js b/www/manager6/storage/BackupView.js
index 8c91ec99..f36030b9 100644
--- a/www/manager6/storage/BackupView.js
+++ b/www/manager6/storage/BackupView.js
@@ -10,6 +10,9 @@ Ext.define('PVE.storage.BackupView', {
initComponent: function () {
let me = this;
+ let content = me.pveSelNode.data.content?.split(',') ?? [];
+ me.hasBackupContent = content.includes('backup');
+
let nodename = (me.nodename = me.pveSelNode.data.node);
if (!nodename) {
throw 'no node name specified';
@@ -79,6 +82,30 @@ Ext.define('PVE.storage.BackupView', {
let isPBS = me.pluginType === 'pbs';
me.tbar = [
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Import'),
+ iconCls: 'fa fa-upload',
+ //disabled: false,
+ selModel: null,
+ disabled: !me.hasBackupContent,
+ requiredPermissions: {
+ check: ['Datastore.Allocate', 'VM.Allocate'],
+ },
+ tooltip: gettext('Import backup and restore VM from local machine'),
+ handler: function () {
+ let win = Ext.create('PVE.window.UploadToStorage', {
+ nodename: me.nodename,
+ storage: me.storage,
+ content: 'import',
+ listeners: {
+ destroy: () => me.store.load(),
+ },
+ });
+ win.show();
+ },
+ },
+ '-',
{
xtype: 'proxmoxButton',
text: gettext('Restore'),
@@ -106,7 +133,7 @@ Ext.define('PVE.storage.BackupView', {
},
});
},
- },
+ }
];
if (isPBS) {
me.tbar.push({
diff --git a/www/manager6/window/Backup.js b/www/manager6/window/Backup.js
index 7c1c54de..d8837d6b 100644
--- a/www/manager6/window/Backup.js
+++ b/www/manager6/window/Backup.js
@@ -41,7 +41,6 @@ Ext.define('PVE.window.Backup', {
xtype: 'proxmoxKVComboBox',
comboItems: [
['notification-system', gettext('Use global settings')],
- // TRANSLATORS: sendmail is a piece of software
['legacy-sendmail', gettext('Use sendmail (legacy)')],
],
fieldLabel: gettext('Notification'),
@@ -245,11 +244,34 @@ Ext.define('PVE.window.Backup', {
padding: '0 0 1 0',
});
+ let exportCheckbox = Ext.create('Proxmox.form.Checkbox', {
+ name: 'export',
+ checked: false,
+ uncheckedValue: 0,
+ tooltip: gettext('If selected Backup is saved to local machine'),
+ fieldLabel: gettext('Export'),
+ // Tiny amount of padding to stop the UI from shifting
+ // when the 'mailto' field is shown.
+ padding: '0 0 1 0',
+ listeners: {
+ change: function (f, checked) {
+
+ //console.log('clicked "export"');
+ storagesel.setDisabled(checked);
+ protectedCheckbox.setHidden(checked);
+ notificationModeSelector.setDisabled(checked);
+ if (checked) {
+ protectedCheckbox.setValue(false);
+ }
+ },
+ },
+ });
+
me.formPanel = Ext.create('Proxmox.panel.InputPanel', {
bodyPadding: 10,
border: false,
column1: [storagesel, modeSelector, protectedCheckbox, pbsChangeDetection],
- column2: [compressionSelector, notificationModeSelector, mailtoField, removeCheckbox],
+ column2: [compressionSelector, notificationModeSelector, exportCheckbox, mailtoField, removeCheckbox],
columnB: [
{
xtype: 'textareafield',
@@ -341,6 +363,13 @@ Ext.define('PVE.window.Backup', {
);
}
+ if (values.export) {
+ let url = `/api2/json/nodes/${me.nodename}/vzdump/export?vmid=${me.vmid}&compress=${values.compress}`;
+ window.open(url, '_blank');
+ me.close();
+ return;
+ }
+
Proxmox.Utils.API2Request({
url: '/nodes/' + me.nodename + '/vzdump',
params: params,
diff --git a/www/manager6/window/UploadToStorage.js b/www/manager6/window/UploadToStorage.js
index cc53596d..7d4bace9 100644
--- a/www/manager6/window/UploadToStorage.js
+++ b/www/manager6/window/UploadToStorage.js
@@ -9,7 +9,7 @@ Ext.define('PVE.window.UploadToStorage', {
title: gettext('Upload'),
acceptedExtensions: {
- import: ['.ova', '.qcow2', '.raw', '.vmdk'],
+ import: ['.ova', '.qcow2', '.raw', '.vmdk', '.vma', '.vma.zst'],
iso: ['.img', '.iso'],
vztmpl: ['.tar.gz', '.tar.xz', '.tar.zst'],
},
@@ -24,14 +24,15 @@ Ext.define('PVE.window.UploadToStorage', {
cbindData: function (initialConfig) {
const me = this;
const ext = me.acceptedExtensions[me.content] || [];
-
+ let fRegex = new RegExp('^.*(?:' + ext.map(e => e.replace(/\./g, '\\.')).join('|') + ')$', 'i');
me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
let fileSelectorExt = ext.concat(Object.keys(me.extensionAliases[me.content] ?? {}));
+
return {
extensions: fileSelectorExt.join(', '),
- filenameRegex: new RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'),
+ filenameRegex: fRegex,
};
},
@@ -45,6 +46,17 @@ Ext.define('PVE.window.UploadToStorage', {
controller: {
submit: function (button) {
+
+ let doUpload = function () {
+ xhr.open('POST', `/api2/json${url}`, true);
+
+ if (window.Proxmox && Proxmox.CSRFPreventionToken) {
+ xhr.setRequestHeader('CSRFPreventionToken', Proxmox.CSRFPreventionToken);
+ }
+
+ xhr.send(fd);
+ };
+
const view = this.getView();
const form = this.lookup('formPanel').getForm();
const abortBtn = this.lookup('abortBtn');
@@ -141,8 +153,49 @@ Ext.define('PVE.window.UploadToStorage', {
false,
);
- xhr.open('POST', `/api2/json${view.url}`, true);
- xhr.send(fd);
+ let url = view.url;
+
+ if (view.content === 'import') {
+ let vmid = null;
+
+ // estrai vmid dal filename tipo vzdump-qemu-100.vma.zst
+ let match = filename.match(/-(\d+)\./);
+ if (match) {
+ vmid = match[1];
+ }
+
+ if (vmid) {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${view.nodename}/qemu/${vmid}/status/current`,
+ method: 'GET',
+
+ success: function () {
+ // VM esiste → chiedi conferma
+ Ext.Msg.confirm(
+ gettext('Confirm Restore'),
+ Ext.String.format(
+ gettext('VM {0} already exists. Overwrite?'),
+ vmid
+ ),
+ function (btn) {
+ if (btn === 'yes') {
+ doUpload();
+ }
+ }
+ );
+ },
+
+ failure: function () {
+ // VM non esiste → procedi direttamente
+ doUpload();
+ },
+ });
+
+ return; // blocca flusso normale
+ }
+ }
+
+ doUpload();
},
validitychange: function (f, valid) {
@@ -327,4 +380,6 @@ Ext.define('PVE.window.UploadToStorage', {
me.callParent();
},
+
+
});
--
2.47.3
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-07-01 8:05 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-26 16:43 [PATCH 0/2] RFC: manager: add direct VM backup export/import support Mauro de Pascale
2026-06-26 16:43 ` [PATCH 1/2] manager: add API endpoint for streamed VM backup export Mauro de Pascale
2026-06-26 16:43 ` [PATCH 2/2] manager: add GUI support for backup export and import Mauro de Pascale
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox