public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: Proxmox VE development discussion <pve-devel@lists.proxmox.com>
Subject: Re: [pve-devel] [POC storage v3 15/34] WIP Borg plugin
Date: Wed, 13 Nov 2024 11:52:58 +0100	[thread overview]
Message-ID: <1731489757.shva7ho8xr.astroid@yuna.none> (raw)
In-Reply-To: <20241107165146.125935-16-f.ebner@proxmox.com>

On November 7, 2024 5:51 pm, Fiona Ebner wrote:
> Archive names start with the guest type and ID and then the same
> timestamp format as PBS.
> 
> Container archives have the following structure:
> guest.config
> firewall.config
> filesystem/ # containing the whole filesystem structure
> 
> VM archives have the following structure
> guest.config
> firewall.config
> volumes/ # containing a raw file for each device
> 
> A bindmount (respectively symlinks) are used to achieve this
> structure, because Borg doesn't seem to support renaming on-the-fly.
> (Prefix stripping via the "slashdot hack" would have helped slightly,
> but is only in Borg >= 1.4
> https://github.com/borgbackup/borg/actions/runs/7967940995)
> 
> NOTE: Bandwidth limit is not yet honored and the task size is not
> calculated yet. Discard for VM backups would also be nice to have, but
> it's not entirely clear how (parsing progress and discarding according
> to that is one idea). There is no dirty bitmap support, not sure if
> that is feasible to add.
> 
> Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
> ---
> 
> Changes in v3:
> * make SSH work.
> * adapt to API changes, i.e. config as raw data and user namespace
>   execution context for containers.
> 
>  src/PVE/API2/Storage/Config.pm         |   2 +-
>  src/PVE/BackupProvider/Plugin/Borg.pm  | 439 ++++++++++++++++++
>  src/PVE/BackupProvider/Plugin/Makefile |   2 +-
>  src/PVE/Storage.pm                     |   2 +
>  src/PVE/Storage/BorgBackupPlugin.pm    | 595 +++++++++++++++++++++++++
>  src/PVE/Storage/Makefile               |   1 +
>  6 files changed, 1039 insertions(+), 2 deletions(-)
>  create mode 100644 src/PVE/BackupProvider/Plugin/Borg.pm
>  create mode 100644 src/PVE/Storage/BorgBackupPlugin.pm
> 
> diff --git a/src/PVE/API2/Storage/Config.pm b/src/PVE/API2/Storage/Config.pm
> index e04b6ab..1cbf09d 100755
> --- a/src/PVE/API2/Storage/Config.pm
> +++ b/src/PVE/API2/Storage/Config.pm
> @@ -190,7 +190,7 @@ __PACKAGE__->register_method ({
>  	return &$api_storage_config($cfg, $param->{storage});
>      }});
>  
> -my $sensitive_params = [qw(password encryption-key master-pubkey keyring)];
> +my $sensitive_params = [qw(password encryption-key master-pubkey keyring ssh-key)];
>  
>  __PACKAGE__->register_method ({
>      name => 'create',
> diff --git a/src/PVE/BackupProvider/Plugin/Borg.pm b/src/PVE/BackupProvider/Plugin/Borg.pm
> new file mode 100644
> index 0000000..7bb3ae3
> --- /dev/null
> +++ b/src/PVE/BackupProvider/Plugin/Borg.pm
> @@ -0,0 +1,439 @@
> +package PVE::BackupProvider::Plugin::Borg;
> +
> +use strict;
> +use warnings;
> +
> +use File::chdir;
> +use File::Basename qw(basename);
> +use File::Path qw(make_path remove_tree);
> +use Net::IP;
> +use POSIX qw(strftime);
> +
> +use PVE::Tools;
> +
> +# ($vmtype, $vmid, $time_string)
> +our $ARCHIVE_RE_3 = qr!^pve-(lxc|qemu)-([0-9]+)-([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)$!;
> +
> +sub archive_name {
> +    my ($vmtype, $vmid, $backup_time) = @_;
> +
> +    return "pve-${vmtype}-${vmid}-" . strftime("%FT%TZ", gmtime($backup_time));
> +}
> +
> +# remove_tree can be very verbose by default, do explicit error handling and limit to one message
> +my sub _remove_tree {
> +    my ($path) = @_;
> +
> +    remove_tree($path, { error => \my $err });
> +    if ($err && @$err) { # empty array if no error
> +	for my $diag (@$err) {
> +	    my ($file, $message) = %$diag;
> +	    die "cannot remove_tree '$path': $message\n" if $file eq '';
> +	    die "cannot remove_tree '$path': unlinking $file failed - $message\n";
> +	}
> +    }
> +}
> +
> +my sub prepare_run_dir {
> +    my ($archive, $operation) = @_;
> +
> +    my $run_dir = "/run/pve-storage-borg-plugin/${archive}.${operation}";
> +    _remove_tree($run_dir);
> +    make_path($run_dir);
> +    die "unable to create directory $run_dir\n" if !-d $run_dir;

this is used as part of restoring - what if I restore the same archive
in parallel into two different VMIDs?

> +
> +    return $run_dir;
> +}
> +
> +my sub log_info {
> +    my ($self, $message) = @_;
> +
> +    $self->{'log-function'}->('info', $message);
> +}
> +
> +my sub log_warning {
> +    my ($self, $message) = @_;
> +
> +    $self->{'log-function'}->('warn', $message);
> +}
> +
> +my sub log_error {
> +    my ($self, $message) = @_;
> +
> +    $self->{'log-function'}->('err', $message);
> +}
> +
> +my sub file_contents_from_archive {
> +    my ($self, $archive, $file) = @_;
> +
> +    my $run_dir = prepare_run_dir($archive, "file-contents");
> +
> +    my $raw;
> +
> +    eval {
> +	local $CWD = $run_dir;
> +
> +	$self->{'storage-plugin'}->borg_cmd_extract(
> +	    $self->{scfg},
> +	    $self->{storeid},
> +	    $archive,
> +	    [$file],
> +	);

borg extract has `--stdout`, which would save writing to the FS here
(since this is only used to extract config file, it should be okay)?

> +
> +	$raw = PVE::Tools::file_get_contents("${run_dir}/${file}");
> +    };
> +    my $err = $@;
> +    eval { _remove_tree($run_dir); };
> +    log_warning($self, $@) if $@;
> +    die $err if $err;
> +
> +    return $raw;
> +}
> +
> +# Plugin implementation
> +
> +sub new {
> +    my ($class, $storage_plugin, $scfg, $storeid, $log_function) = @_;
> +
> +    my $self = bless {
> +	scfg => $scfg,
> +	storeid => $storeid,
> +	'storage-plugin' => $storage_plugin,
> +	'log-function' => $log_function,
> +    }, $class;
> +
> +    return $self;
> +}
> +
> +sub provider_name {
> +    my ($self) = @_;
> +
> +    return "Borg";
> +}
> +
> +sub job_hook {
> +    my ($self, $phase, $info) = @_;
> +
> +    if ($phase eq 'start') {
> +	$self->{'job-id'} = $info->{'start-time'};
> +	$self->{password} = $self->{'storage-plugin'}->borg_get_password(
> +	    $self->{scfg}, $self->{storeid});
> +	$self->{'ssh-key-fh'} = $self->{'storage-plugin'}->borg_open_ssh_key(
> +	    $self->{scfg}, $self->{storeid});
> +    } else {
> +	delete $self->{password};

why do we delete this, but don't close the ssh-key-fh ?

> +    }
> +
> +    return;
> +}
> +
> +sub backup_hook {
> +    my ($self, $phase, $vmid, $vmtype, $info) = @_;
> +
> +    if ($phase eq 'start') {
> +	$self->{$vmid}->{'task-size'} = 0;
> +    } elsif ($phase eq 'prepare') {
> +	if ($vmtype eq 'lxc') {
> +	    my $archive = $self->{$vmid}->{archive};
> +	    my $run_dir = prepare_run_dir($archive, "backup-container");
> +	    $self->{$vmid}->{'run-dir'} = $run_dir;
> +
> +	    my $create_dir = sub {
> +		my $dir = shift;
> +		make_path($dir);
> +		die "unable to create directory $dir\n" if !-d $dir;
> +		chown($info->{'backup-user-id'}, -1, $dir)
> +		    or die "unable to change owner for $dir\n";
> +	    };
> +
> +	    $create_dir->("${run_dir}/backup/");
> +	    $create_dir->("${run_dir}/backup/filesystem");
> +	    $create_dir->("${run_dir}/ssh");
> +	    $create_dir->("${run_dir}/.config");
> +	    $create_dir->("${run_dir}/.cache");

so this is a bit tricky.. we need unpriv access (to do the backup), but
we store sensitive things here that we don't actually want to hand out
to everyone..

> +
> +	    for my $subdir ($info->{sources}->@*) {
> +		PVE::Tools::run_command([
> +		    'mount',
> +		    '-o', 'bind,ro',
> +		    "$info->{directory}/${subdir}",
> +		    "${run_dir}/backup/filesystem/${subdir}",
> +		]);
> +	    }
> +	}
> +    } elsif ($phase eq 'end' || $phase eq 'abort') {
> +	if ($vmtype eq 'lxc') {
> +	    my $run_dir = $self->{$vmid}->{'run-dir'};
> +	    eval {
> +		eval { PVE::Tools::run_command(['umount', "${run_dir}/ssh"]); };

this might warrant a comment ;) a tmpfs is mounted there in
backup_container..

> +		eval { PVE::Tools::run_command(['umount', '-R', "${run_dir}/backup/filesystem"]); };
> +		_remove_tree($run_dir);
> +	    };
> +	    die "unable to clean up $run_dir - $@" if $@;
> +	}
> +    }
> +
> +    return;
> +}
> +
> +sub backup_get_mechanism {
> +    my ($self, $vmid, $vmtype) = @_;
> +
> +    return ('block-device', undef) if $vmtype eq 'qemu';
> +    return ('directory', undef) if $vmtype eq 'lxc';
> +
> +    die "unsupported VM type '$vmtype'\n";
> +}
> +
> +sub backup_get_archive_name {
> +    my ($self, $vmid, $vmtype, $backup_time) = @_;
> +
> +    return $self->{$vmid}->{archive} = archive_name($vmtype, $vmid, $backup_time);
> +}
> +
> +sub backup_get_task_size {
> +    my ($self, $vmid) = @_;
> +
> +    return $self->{$vmid}->{'task-size'};
> +}
> +
> +sub backup_handle_log_file {
> +    my ($self, $vmid, $filename) = @_;
> +
> +    return; # don't upload, Proxmox VE keeps the task log too
> +}
> +
> +sub backup_vm {
> +    my ($self, $vmid, $guest_config, $volumes, $info) = @_;
> +
> +    # TODO honor bandwith limit
> +    # TODO discard?
> +
> +    my $archive = $self->{$vmid}->{archive};
> +
> +    my $run_dir = prepare_run_dir($archive, "backup-vm");
> +    my $volume_dir = "${run_dir}/volumes";
> +    make_path($volume_dir);
> +    die "unable to create directory $volume_dir\n" if !-d $volume_dir;
> +
> +    PVE::Tools::file_set_contents("${run_dir}/guest.config", $guest_config);

same here

> +    my $paths = ['./guest.config'];
> +
> +    if (my $firewall_config = $info->{'firewall-config'}) {
> +	PVE::Tools::file_set_contents("${run_dir}/firewall.config", $firewall_config);

and here - these paths are world-readable by default..

> +	push $paths->@*, './firewall.config';
> +    }
> +
> +    for my $devicename (sort keys $volumes->%*) {
> +	my $path = $volumes->{$devicename}->{path};
> +	my $link_name = "${volume_dir}/${devicename}.raw";
> +	symlink($path, $link_name) or die "could not create symlink $link_name -> $path\n";
> +	push $paths->@*, "./volumes/" . basename($link_name, ());
> +    }
> +
> +    # TODO --stats for size?
> +
> +    eval {
> +	local $CWD = $run_dir;
> +
> +	$self->{'storage-plugin'}->borg_cmd_create(
> +	    $self->{scfg},
> +	    $self->{storeid},
> +	    $self->{$vmid}->{archive},
> +	    $paths,
> +	    ['--read-special', '--progress'],
> +	);
> +    };
> +    my $err = $@;
> +    eval { _remove_tree($run_dir) };
> +    log_warning($self, $@) if $@;
> +    die $err if $err;
> +}
> +
> +sub backup_container {
> +    my ($self, $vmid, $guest_config, $exclude_patterns, $info) = @_;
> +
> +    # TODO honor bandwith limit
> +
> +    my $run_dir = $self->{$vmid}->{'run-dir'};
> +    my $backup_dir = "${run_dir}/backup";
> +
> +    my $archive = $self->{$vmid}->{archive};
> +
> +    PVE::Tools::run_command(['mount', '-t', 'tmpfs', '-o', 'size=1M', 'tmpfs', "${run_dir}/ssh"]);
> +
> +    if ($self->{'ssh-key-fh'}) {
> +	my $ssh_key =
> +	    PVE::Tools::safe_read_from($self->{'ssh-key-fh'}, 1024 * 1024, 0, "SSH key file");
> +	PVE::Tools::file_set_contents("${run_dir}/ssh/ssh.key", $ssh_key, 0600);

okay, so this should be fine..

> +    }
> +
> +    if (my $ssh_fingerprint = $self->{scfg}->{'ssh-fingerprint'}) {
> +	my ($server, $port) = $self->{scfg}->@{qw(server port)};
> +	$server = "[$server]" if Net::IP::ip_is_ipv6($server);
> +	$server = "${server}:${port}" if $port;
> +	my $fp_line = "$server $ssh_fingerprint\n";
> +	PVE::Tools::file_set_contents("${run_dir}/ssh/known_hosts", $fp_line, 0600);
> +    }
> +
> +    PVE::Tools::file_set_contents("${backup_dir}/guest.config", $guest_config);

but this

> +    my $paths = ['./guest.config'];
> +
> +    if (my $firewall_config = $info->{'firewall-config'}) {
> +	PVE::Tools::file_set_contents("${backup_dir}/firewall.config", $firewall_config);

and this should also be 0600? or we could chmod the dirs themselves when
creating, to avoid missing paths?

> +	push $paths->@*, './firewall.config';
> +    }
> +
> +    push $paths->@*, "./filesystem";
> +
> +    my $opts = ['--numeric-ids', '--sparse', '--progress'];
> +
> +    for my $pattern ($exclude_patterns->@*) {
> +	if ($pattern =~ m|^/|) {
> +	    push $opts->@*, '-e', "filesystem${pattern}";
> +	} else {
> +	    push $opts->@*, '-e', "filesystem/**${pattern}";
> +	}
> +    }
> +
> +    push $opts->@*, '-e', "filesystem/**lost+found" if $info->{'backup-user-id'} != 0;
> +
> +    # TODO --stats for size?
> +
> +    # Don't make it local to avoid permission denied error when changing back, because the method is
> +    # executed in a user namespace.
> +    $CWD = $backup_dir if $info->{'backup-user-id'} != 0;
> +    {
> +	local $CWD = $backup_dir;
> +	local $ENV{BORG_BASE_DIR} = ${run_dir};
> +	local $ENV{BORG_PASSPHRASE} = $self->{password};
> +
> +	local $ENV{BORG_RSH} =
> +	    "ssh -o \"UserKnownHostsFile ${run_dir}/ssh/known_hosts\" -i ${run_dir}/ssh/ssh.key";
> +
> +	$self->{'storage-plugin'}->borg_cmd_create(
> +	    $self->{scfg},
> +	    $self->{storeid},
> +	    $self->{$vmid}->{archive},
> +	    $paths,
> +	    $opts,
> +	);
> +    }
> +}
> +
> +sub restore_get_mechanism {
> +    my ($self, $volname, $storeid) = @_;
> +
> +    my (undef, $archive) = $self->{'storage-plugin'}->parse_volname($volname);
> +    my ($vmtype) = $archive =~ m!^pve-([^\s-]+)!
> +	or die "cannot parse guest type from archive name '$archive'\n";
> +
> +    return ('qemu-img', $vmtype) if $vmtype eq 'qemu';
> +    return ('directory', $vmtype) if $vmtype eq 'lxc';
> +
> +    die "unexpected guest type '$vmtype'\n";
> +}
> +
> +sub restore_get_guest_config {
> +    my ($self, $volname, $storeid) = @_;
> +
> +    my (undef, $archive) = $self->{'storage-plugin'}->parse_volname($volname);
> +    return file_contents_from_archive($self, $archive, 'guest.config');
> +}
> +
> +sub restore_get_firewall_config {
> +    my ($self, $volname, $storeid) = @_;
> +
> +    my (undef, $archive) = $self->{'storage-plugin'}->parse_volname($volname);
> +    my $config = eval {
> +	file_contents_from_archive($self, $archive, 'firewall.config');
> +    };
> +    if (my $err = $@) {
> +	return if $err =~ m!Include pattern 'firewall\.config' never matched\.!;
> +	die $err;
> +    }
> +    return $config;
> +}
> +
> +sub restore_vm_init {
> +    my ($self, $volname, $storeid) = @_;
> +
> +    my $res = {};
> +
> +    my (undef, $archive, $vmid) = $self->{'storage-plugin'}->parse_volname($volname);
> +    my $mount_point = prepare_run_dir($archive, "restore-vm");
> +
> +    $self->{'storage-plugin'}->borg_cmd_mount(
> +	$self->{scfg},
> +	$self->{storeid},
> +	$archive,
> +	$mount_point,
> +    );

haven't actually tested this code, but what are the permissions like for
this mounted backup archive contents? we don't want to expose guest
volumes as world-readable either..

> +
> +    my @backup_files = glob("$mount_point/volumes/*");
> +    for my $backup_file (@backup_files) {
> +	next if $backup_file !~ m!^(.*/(.*)\.raw)$!; # untaint
> +	($backup_file, my $devicename) = ($1, $2);
> +	# TODO avoid dependency on base plugin?
> +	$res->{$devicename}->{size} = PVE::Storage::Plugin::file_size_info($backup_file);
> +    }
> +
> +    $self->{$volname}->{'mount-point'} = $mount_point;
> +
> +    return $res;
> +}
> +
> +sub restore_vm_cleanup {
> +    my ($self, $volname, $storeid) = @_;
> +
> +    my $mount_point = $self->{$volname}->{'mount-point'} or return;
> +
> +    PVE::Tools::run_command(['umount', $mount_point]);
> +
> +    return;
> +}
> +
> +sub restore_vm_volume_init {
> +    my ($self, $volname, $storeid, $devicename, $info) = @_;
> +
> +    my $mount_point = $self->{$volname}->{'mount-point'}
> +	or die "expected mount point for archive not present\n";
> +
> +    return { 'qemu-img-path' => "${mount_point}/volumes/${devicename}.raw" };
> +}
> +
> +sub restore_vm_volume_cleanup {
> +    my ($self, $volname, $storeid, $devicename, $info) = @_;
> +
> +    return;
> +}
> +
> +sub restore_container_init {
> +    my ($self, $volname, $storeid, $info) = @_;
> +
> +    my (undef, $archive, $vmid) = $self->{'storage-plugin'}->parse_volname($volname);
> +    my $mount_point = prepare_run_dir($archive, "restore-container");
> +
> +    $self->{'storage-plugin'}->borg_cmd_mount(
> +	$self->{scfg},
> +	$self->{storeid},
> +	$archive,
> +	$mount_point,
> +    );

same question here..

> +
> +    $self->{$volname}->{'mount-point'} = $mount_point;
> +
> +    return { 'archive-directory' => "${mount_point}/filesystem" };
> +}
> +
> +sub restore_container_cleanup {
> +    my ($self, $volname, $storeid, $info) = @_;
> +
> +    my $mount_point = $self->{$volname}->{'mount-point'} or return;
> +
> +    PVE::Tools::run_command(['umount', $mount_point]);
> +
> +    return;
> +}
> +
> +1;
> diff --git a/src/PVE/BackupProvider/Plugin/Makefile b/src/PVE/BackupProvider/Plugin/Makefile
> index bedc26e..db08c2d 100644
> --- a/src/PVE/BackupProvider/Plugin/Makefile
> +++ b/src/PVE/BackupProvider/Plugin/Makefile
> @@ -1,4 +1,4 @@
> -SOURCES = Base.pm DirectoryExample.pm
> +SOURCES = Base.pm Borg.pm DirectoryExample.pm
>  
>  .PHONY: install
>  install:
> diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
> index 9f9a86b..f4bfc55 100755
> --- a/src/PVE/Storage.pm
> +++ b/src/PVE/Storage.pm
> @@ -40,6 +40,7 @@ use PVE::Storage::ZFSPlugin;
>  use PVE::Storage::PBSPlugin;
>  use PVE::Storage::BTRFSPlugin;
>  use PVE::Storage::ESXiPlugin;
> +use PVE::Storage::BorgBackupPlugin;
>  
>  # Storage API version. Increment it on changes in storage API interface.
>  use constant APIVER => 11;
> @@ -66,6 +67,7 @@ PVE::Storage::ZFSPlugin->register();
>  PVE::Storage::PBSPlugin->register();
>  PVE::Storage::BTRFSPlugin->register();
>  PVE::Storage::ESXiPlugin->register();
> +PVE::Storage::BorgBackupPlugin->register();
>  
>  # load third-party plugins
>  if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) {
> diff --git a/src/PVE/Storage/BorgBackupPlugin.pm b/src/PVE/Storage/BorgBackupPlugin.pm
> new file mode 100644
> index 0000000..8f0e721
> --- /dev/null
> +++ b/src/PVE/Storage/BorgBackupPlugin.pm
> @@ -0,0 +1,595 @@
> +package PVE::Storage::BorgBackupPlugin;
> +
> +use strict;
> +use warnings;
> +
> +use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
> +use JSON qw(from_json);
> +use MIME::Base64 qw(decode_base64);
> +use Net::IP;
> +use POSIX;
> +
> +use PVE::BackupProvider::Plugin::Borg;
> +use PVE::Tools;
> +
> +use base qw(PVE::Storage::Plugin);
> +
> +my sub borg_repository_uri {
> +    my ($scfg, $storeid) = @_;
> +
> +    my $uri = '';
> +    my $server = $scfg->{server} or die "no server configured for $storeid\n";
> +    my $username = $scfg->{username} or die "no username configured for $storeid\n";
> +    my $prefix = "ssh://$username@";
> +    $server = "[$server]" if Net::IP::ip_is_ipv6($server);
> +    if (my $port = $scfg->{port}) {
> +	$uri = "${prefix}${server}:${port}";
> +    } else {
> +	$uri = "${prefix}${server}";
> +    }
> +    $uri .= $scfg->{'repository-path'};
> +
> +    return $uri;
> +}
> +
> +my sub borg_password_file_name {
> +    my ($scfg, $storeid) = @_;
> +
> +    return "/etc/pve/priv/storage/${storeid}.pw";
> +}
> +
> +my sub borg_set_password {
> +    my ($scfg, $storeid, $password) = @_;
> +
> +    my $pwfile = borg_password_file_name($scfg, $storeid);
> +    mkdir "/etc/pve/priv/storage";
> +
> +    PVE::Tools::file_set_contents($pwfile, "$password\n");
> +}
> +
> +my sub borg_delete_password {
> +    my ($scfg, $storeid) = @_;
> +
> +    my $pwfile = borg_password_file_name($scfg, $storeid);
> +
> +    unlink $pwfile;
> +}
> +
> +sub borg_get_password {
> +    my ($class, $scfg, $storeid) = @_;
> +
> +    my $pwfile = borg_password_file_name($scfg, $storeid);
> +
> +    return PVE::Tools::file_read_firstline($pwfile);
> +}
> +
> +sub borg_cmd_list {
> +    my ($class, $scfg, $storeid) = @_;
> +
> +    my $uri = borg_repository_uri($scfg, $storeid);
> +
> +    local $ENV{BORG_PASSPHRASE} = $class->borg_get_password($scfg, $storeid)
> +	if !$ENV{BORG_PASSPHRASE};
> +
> +    my $json = '';
> +    my $cmd = ['borg', 'list', '--json', $uri];
> +
> +    my $errfunc = sub { warn $_[0]; };
> +    my $outfunc = sub { $json .= $_[0]; };
> +
> +    PVE::Tools::run_command(
> +	$cmd, errmsg => "command @$cmd failed", outfunc => $outfunc, errfunc => $errfunc);
> +
> +    my $res = eval { from_json($json) };
> +    die "unable to parse 'borg list' output - $@\n" if $@;
> +    return $res;
> +}
> +
> +sub borg_cmd_create {
> +    my ($class, $scfg, $storeid, $archive, $paths, $opts) = @_;
> +
> +    my $uri = borg_repository_uri($scfg, $storeid);
> +
> +    local $ENV{BORG_PASSPHRASE} = $class->borg_get_password($scfg, $storeid)
> +	if !$ENV{BORG_PASSPHRASE};
> +
> +    my $cmd = ['borg', 'create', $opts->@*, "${uri}::${archive}", $paths->@*];
> +
> +    PVE::Tools::run_command($cmd, errmsg => "command @$cmd failed");
> +
> +    return;
> +}
> +
> +sub borg_cmd_extract {
> +    my ($class, $scfg, $storeid, $archive, $paths) = @_;
> +
> +    my $uri = borg_repository_uri($scfg, $storeid);
> +
> +    local $ENV{BORG_PASSPHRASE} = $class->borg_get_password($scfg, $storeid)
> +	if !$ENV{BORG_PASSPHRASE};
> +
> +    my $cmd = ['borg', 'extract', "${uri}::${archive}", $paths->@*];
> +
> +    PVE::Tools::run_command($cmd, errmsg => "command @$cmd failed");
> +
> +    return;
> +}
> +
> +sub borg_cmd_delete {
> +    my ($class, $scfg, $storeid, $archive) = @_;
> +
> +    my $uri = borg_repository_uri($scfg, $storeid);
> +
> +    local $ENV{BORG_PASSPHRASE} = $class->borg_get_password($scfg, $storeid)
> +	if !$ENV{BORG_PASSPHRASE};
> +
> +    my $cmd = ['borg', 'delete', "${uri}::${archive}"];
> +
> +    PVE::Tools::run_command($cmd, errmsg => "command @$cmd failed");
> +
> +    return;
> +}
> +
> +sub borg_cmd_info {
> +    my ($class, $scfg, $storeid, $archive, $timeout) = @_;
> +
> +    my $uri = borg_repository_uri($scfg, $storeid);
> +
> +    local $ENV{BORG_PASSPHRASE} = $class->borg_get_password($scfg, $storeid)
> +	if !$ENV{BORG_PASSPHRASE};
> +
> +    my $json = '';
> +    my $cmd = ['borg', 'info', '--json', "${uri}::${archive}"];
> +
> +    my $errfunc = sub { warn $_[0]; };
> +    my $outfunc = sub { $json .= $_[0]; };
> +
> +    PVE::Tools::run_command(
> +	$cmd,
> +	errmsg => "command @$cmd failed",
> +	timeout => $timeout,
> +	outfunc => $outfunc,
> +	errfunc => $errfunc,
> +    );
> +
> +    my $res = eval { from_json($json) };
> +    die "unable to parse 'borg info' output for archive '$archive' - $@\n" if $@;
> +    return $res;
> +}
> +
> +sub borg_cmd_mount {
> +    my ($class, $scfg, $storeid, $archive, $mount_point) = @_;
> +
> +    my $uri = borg_repository_uri($scfg, $storeid);
> +
> +    local $ENV{BORG_PASSPHRASE} = $class->borg_get_password($scfg, $storeid)
> +	if !$ENV{BORG_PASSPHRASE};
> +
> +    my $cmd = ['borg', 'mount', "${uri}::${archive}", $mount_point];
> +
> +    PVE::Tools::run_command($cmd, errmsg => "command @$cmd failed");
> +
> +    return;
> +}
> +
> +my sub parse_backup_time {
> +    my ($time_string) = @_;
> +
> +    my @tm = (POSIX::strptime($time_string, "%FT%TZ"));
> +    # expect sec, min, hour, mday, mon, year
> +    if (grep { !defined($_) } @tm[0..5]) {
> +	warn "error parsing time from string '$time_string'\n";
> +	return 0;
> +    } else {
> +	local $ENV{TZ} = 'UTC'; # time string is UTC
> +
> +	# Fill in isdst to avoid undef warning. No daylight saving time for UTC.
> +	$tm[8] //= 0;
> +
> +	if (my $since_epoch = mktime(@tm)) {
> +	    return int($since_epoch);
> +	} else {
> +	    warn "error parsing time from string '$time_string'\n";
> +	    return 0;
> +	}
> +    }
> +}
> +
> +# Helpers
> +
> +sub type {
> +    return 'borg';
> +}
> +
> +sub plugindata {
> +    return {
> +	content => [ { backup => 1, none => 1 }, { backup => 1 } ],
> +	features => { 'backup-provider' => 1 },
> +    };
> +}
> +
> +sub properties {
> +    return {
> +	'repository-path' => {
> +	    description => "Path to the backup repository",
> +	    type => 'string',
> +	},
> +	'ssh-key' => {
> +	    description => "FIXME", # FIXME
> +	    type => 'string',
> +	},
> +	'ssh-fingerprint' => {
> +	    description => "FIXME", # FIXME
> +	    type => 'string',
> +	},

these should probably get descriptions and formats, but this is titled
WIP :)

> +    };
> +}
> +
> +sub options {
> +    return {
> +	'repository-path' => { fixed => 1 },
> +	server => { fixed => 1 },
> +	port => { optional => 1 },
> +	username => { fixed => 1 },
> +	'ssh-key' => { optional => 1 },
> +	'ssh-fingerprint' => { optional => 1 },
> +	password => { optional => 1 },
> +	disable => { optional => 1 },
> +	nodes => { optional => 1 },
> +	'prune-backups' => { optional => 1 },
> +	'max-protected-backups' => { optional => 1 },
> +    };
> +}
> +
> +sub borg_ssh_key_file_name {
> +    my ($scfg, $storeid) = @_;
> +
> +    return "/etc/pve/priv/storage/${storeid}.ssh.key";
> +}
> +
> +sub borg_set_ssh_key {
> +    my ($scfg, $storeid, $key) = @_;
> +
> +    my $pwfile = borg_ssh_key_file_name($scfg, $storeid);

nit: variable name

> +    mkdir "/etc/pve/priv/storage";
> +
> +    PVE::Tools::file_set_contents($pwfile, "$key\n");
> +}
> +
> +sub borg_delete_ssh_key {
> +    my ($scfg, $storeid) = @_;
> +
> +    my $pwfile = borg_ssh_key_file_name($scfg, $storeid);

same

> +
> +    if (!unlink $pwfile) {
> +	return if $! == ENOENT;
> +	die "failed to delete SSH key! $!\n";
> +    }
> +    delete $scfg->{'ssh-key'};
> +}
> +
> +sub borg_get_ssh_key {
> +    my ($scfg, $storeid) = @_;
> +
> +    my $pwfile = borg_ssh_key_file_name($scfg, $storeid);

same

> +
> +    return PVE::Tools::file_get_contents($pwfile);
> +}
> +
> +# Returns a file handle with FD_CLOEXEC disabled if there is an SSH key , or `undef` if there is
> +# not. Dies on error.
> +sub borg_open_ssh_key {
> +    my ($self, $scfg, $storeid) = @_;
> +
> +    my $ssh_key_file = borg_ssh_key_file_name($scfg, $storeid);
> +
> +    my $keyfd;
> +    if (!open($keyfd, '<', $ssh_key_file)) {
> +	if ($! == ENOENT) {
> +	    die "SSH key configured but no key file found!\n" if $scfg->{'ssh-key'};
> +	    return undef;
> +	}
> +	die "failed to open SSH key: $ssh_key_file: $!\n";
> +    }
> +    my $flags = fcntl($keyfd, F_GETFD, 0)
> +	// die "failed to get file descriptor flags for SSH key FD: $!\n";
> +    fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
> +	or die "failed to remove FD_CLOEXEC from SSH key file descriptor\n";
> +
> +    return $keyfd;
> +}
> +
> +# Storage implementation
> +
> +sub on_add_hook {
> +    my ($class, $storeid, $scfg, %param) = @_;
> +
> +    if (defined(my $password = $param{password})) {
> +	borg_set_password($scfg, $storeid, $password);
> +    } else {
> +	borg_delete_password($scfg, $storeid);
> +    }
> +
> +    if (defined(my $ssh_key = delete $param{'ssh-key'})) {
> +	my $decoded = decode_base64($ssh_key);
> +	borg_set_ssh_key($scfg, $storeid, $decoded);
> +	$scfg->{'ssh-key'} = 1;
> +    } else {
> +	borg_delete_ssh_key($scfg, $storeid);
> +    }
> +
> +    return;
> +}
> +
> +sub on_update_hook {
> +    my ($class, $storeid, $scfg, %param) = @_;
> +
> +    if (exists($param{password})) {
> +	if (defined($param{password})) {
> +	    borg_set_password($scfg, $storeid, $param{password});
> +	} else {
> +	    borg_delete_password($scfg, $storeid);
> +	}
> +    }
> +
> +    if (exists($param{'ssh-key'})) {
> +	if (defined(my $ssh_key = delete($param{'ssh-key'}))) {
> +	    my $decoded = decode_base64($ssh_key);
> +
> +	    borg_set_ssh_key($scfg, $storeid, $decoded);
> +	    $scfg->{'ssh-key'} = 1;
> +	} else {
> +	    borg_delete_ssh_key($scfg, $storeid);
> +	}
> +    }
> +
> +    return;
> +}
> +
> +sub on_delete_hook {
> +    my ($class, $storeid, $scfg) = @_;
> +
> +    borg_delete_password($scfg, $storeid);
> +    borg_delete_ssh_key($scfg, $storeid);
> +
> +    return;
> +}
> +
> +sub prune_backups {
> +    my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
> +
> +    # FIXME - is 'borg prune' compatible with ours?
> +    die "not implemented";
> +}
> +
> +sub parse_volname {
> +    my ($class, $volname) = @_;
> +
> +    if ($volname =~ m!^backup/(.*)$!) {
> +	my $archive = $1;
> +	if ($archive =~ $PVE::BackupProvider::Plugin::Borg::ARCHIVE_RE_3) {
> +	    return ('backup', $archive, $2);
> +	}
> +    }
> +
> +    die "unable to parse Borg volume name '$volname'\n";
> +}
> +
> +sub path {
> +    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
> +
> +    die "volume snapshot is not possible on Borg volume" if $snapname;
> +
> +    my $uri = borg_repository_uri($scfg, $storeid);
> +    my (undef, $archive) = $class->parse_volname($volname);
> +
> +    return "${uri}::${archive}";
> +}
> +
> +sub create_base {
> +    my ($class, $storeid, $scfg, $volname) = @_;
> +
> +    die "cannot create base image in Borg storage\n";
> +}
> +
> +sub clone_image {
> +    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
> +
> +    die "can't clone images in Borg storage\n";
> +}
> +
> +sub alloc_image {
> +    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
> +
> +    die "can't allocate space in Borg storage\n";
> +}
> +
> +sub free_image {
> +    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
> +
> +    my (undef, $archive) = $class->parse_volname($volname);
> +
> +    borg_cmd_delete($class, $scfg, $storeid, $archive);
> +
> +    return;
> +}
> +
> +sub list_images {
> +    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
> +
> +    return []; # guest images are not supported, only backups
> +}
> +
> +sub list_volumes {
> +    my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
> +
> +    my $res = [];
> +
> +    return $res if !grep { $_ eq 'backup' } $content_types->@*;
> +
> +    my $archives = $class->borg_cmd_list($scfg, $storeid)->{archives}
> +	or die "expected 'archives' key in 'borg list' JSON output missing\n";
> +
> +    for my $info ($archives->@*) {
> +	my $archive = $info->{archive};
> +	my ($vmtype, $backup_vmid, $time_string) =
> +	    $archive =~ $PVE::BackupProvider::Plugin::Borg::ARCHIVE_RE_3 or next;
> +
> +	next if defined($vmid) && $vmid != $backup_vmid;
> +
> +	push $res->@*, {
> +	    volid => "${storeid}:backup/${archive}",
> +	    size => 0, # FIXME how to cheaply get?
> +	    content => 'backup',
> +	    ctime => parse_backup_time($time_string),
> +	    vmid => $backup_vmid,
> +	    format => "borg-archive",
> +	    subtype => $vmtype,
> +	}
> +    }
> +
> +    return $res;
> +}
> +
> +sub status {
> +    my ($class, $storeid, $scfg, $cache) = @_;
> +
> +    my $uri = borg_repository_uri($scfg, $storeid);
> +
> +    my $res;
> +
> +    if ($uri =~ m!^ssh://!) {
> +	#FIXME ssh and df on target?

borg targets will often be locked down to only allow executing borg on
the other end though..

I am not sure what makes sense here tbh..

> +	return;
> +    } else { # $uri is a local path
> +	my $timeout = 2;
> +	$res = PVE::Tools::df($uri, $timeout);
> +
> +	return if !$res || !$res->{total};
> +    }
> +
> +
> +    return ($res->{total}, $res->{avail}, $res->{used}, 1);
> +}
> +
> +sub activate_storage {
> +    my ($class, $storeid, $scfg, $cache) = @_;
> +
> +    # TODO how to cheaply check? split ssh and non-ssh?
> +
> +    return 1;
> +}
> +
> +sub deactivate_storage {
> +    my ($class, $storeid, $scfg, $cache) = @_;
> +
> +    return 1;
> +}
> +
> +sub activate_volume {
> +    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
> +
> +    die "volume snapshot is not possible on Borg volume" if $snapname;
> +
> +    return 1;
> +}
> +
> +sub deactivate_volume {
> +    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
> +
> +    die "volume snapshot is not possible on Borg volume" if $snapname;
> +
> +    return 1;
> +}
> +
> +sub get_volume_attribute {
> +    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
> +
> +    return;
> +}
> +
> +sub update_volume_attribute {
> +    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
> +
> +    # FIXME notes or protected possible?
> +
> +    die "attribute '$attribute' is not supported on Borg volume";
> +}
> +
> +sub volume_size_info {
> +    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
> +
> +    my (undef, $archive) = $class->parse_volname($volname);
> +    my (undef, undef, $time_string) =
> +	$archive =~ $PVE::BackupProvider::Plugin::Borg::ARCHIVE_RE_3;
> +
> +    my $backup_time = 0;
> +    if ($time_string) {
> +	$backup_time = parse_backup_time($time_string)
> +    } else {
> +	warn "could not parse time from archive name '$archive'\n";
> +    }
> +
> +    my $archives = borg_cmd_info($class, $scfg, $storeid, $archive, $timeout)->{archives}
> +	or die "expected 'archives' key in 'borg info' JSON output missing\n";
> +
> +    my $stats = eval { $archives->[0]->{stats} }
> +	or die "expected entry in 'borg info' JSON output missing\n";
> +    my ($size, $used) = $stats->@{qw(original_size deduplicated_size)};
> +
> +    ($size) = ($size =~ /^(\d+)$/); # untaint
> +    die "size '$size' not an integer\n" if !defined($size);
> +    # coerce back from string
> +    $size = int($size);
> +    ($used) = ($used =~ /^(\d+)$/); # untaint
> +    die "used '$used' not an integer\n" if !defined($used);
> +    # coerce back from string
> +    $used = int($used);
> +
> +    return wantarray ? ($size, 'borg-archive', $used, undef, $backup_time) : $size;
> +}
> +
> +sub volume_resize {
> +    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
> +
> +    die "volume resize is not possible on Borg volume";
> +}
> +
> +sub volume_snapshot {
> +    my ($class, $scfg, $storeid, $volname, $snap) = @_;
> +
> +    die "volume snapshot is not possible on Borg volume";
> +}
> +
> +sub volume_snapshot_rollback {
> +    my ($class, $scfg, $storeid, $volname, $snap) = @_;
> +
> +    die "volume snapshot rollback is not possible on Borg volume";
> +}
> +
> +sub volume_snapshot_delete {
> +    my ($class, $scfg, $storeid, $volname, $snap) = @_;
> +
> +    die "volume snapshot delete is not possible on Borg volume";
> +}
> +
> +sub volume_has_feature {
> +    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
> +
> +    return 0;
> +}
> +
> +sub rename_volume {
> +    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
> +
> +    die "volume rename is not implemented in Borg storage plugin\n";
> +}
> +
> +sub new_backup_provider {
> +    my ($class, $scfg, $storeid, $bandwidth_limit, $log_function) = @_;
> +
> +    return PVE::BackupProvider::Plugin::Borg->new(
> +	$class, $scfg, $storeid, $bandwidth_limit, $log_function);
> +}
> +
> +1;
> diff --git a/src/PVE/Storage/Makefile b/src/PVE/Storage/Makefile
> index acd37f4..9fe2c66 100644
> --- a/src/PVE/Storage/Makefile
> +++ b/src/PVE/Storage/Makefile
> @@ -14,6 +14,7 @@ SOURCES= \
>  	PBSPlugin.pm \
>  	BTRFSPlugin.pm \
>  	LvmThinPlugin.pm \
> +	BorgBackupPlugin.pm \

do we want this one here, while the other one is in Custom?

>  	ESXiPlugin.pm
>  
>  .PHONY: install
> -- 
> 2.39.5
> 
> 
> 
> _______________________________________________
> pve-devel mailing list
> pve-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
> 
> 
> 


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


  reply	other threads:[~2024-11-13 10:53 UTC|newest]

Thread overview: 63+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-11-07 16:51 [pve-devel] [RFC qemu/common/storage/qemu-server/container/manager v3 00/34] backup provider API Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [PATCH qemu v3 01/34] block/reqlist: allow adding overlapping requests Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [PATCH qemu v3 02/34] PVE backup: fixup error handling for fleecing Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [PATCH qemu v3 03/34] PVE backup: factor out setting up snapshot access " Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [PATCH qemu v3 04/34] PVE backup: save device name in device info structure Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [PATCH qemu v3 05/34] PVE backup: include device name in error when setting up snapshot access fails Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC qemu v3 06/34] PVE backup: add target ID in backup state Fiona Ebner
2024-11-12 16:46   ` Fabian Grünbichler
2024-11-13  9:22     ` Fiona Ebner
2024-11-13  9:33       ` Fiona Ebner
2024-11-13 11:16       ` Fabian Grünbichler
2024-11-13 11:40         ` Fiona Ebner
2024-11-13 12:03           ` Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [RFC qemu v3 07/34] PVE backup: get device info: allow caller to specify filter for which devices use fleecing Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC qemu v3 08/34] PVE backup: implement backup access setup and teardown API for external providers Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC qemu v3 09/34] PVE backup: implement bitmap support for external backup access Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC common v3 10/34] env: add module with helpers to run a Perl subroutine in a user namespace Fiona Ebner
2024-11-11 18:33   ` Thomas Lamprecht
2024-11-12 10:19     ` Fiona Ebner
2024-11-12 14:20   ` Fabian Grünbichler
2024-11-13 10:08     ` Fiona Ebner
2024-11-13 11:15       ` Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [RFC storage v3 11/34] add storage_has_feature() helper function Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC storage v3 12/34] plugin: introduce new_backup_provider() method Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC storage v3 13/34] extract backup config: delegate to backup provider for storages that support it Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [POC storage v3 14/34] add backup provider example Fiona Ebner
2024-11-13 10:52   ` Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [POC storage v3 15/34] WIP Borg plugin Fiona Ebner
2024-11-13 10:52   ` Fabian Grünbichler [this message]
2024-11-07 16:51 ` [pve-devel] [PATCH qemu-server v3 16/34] move nbd_stop helper to QMPHelpers module Fiona Ebner
2024-11-11 13:55   ` [pve-devel] applied: " Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [PATCH qemu-server v3 17/34] backup: move cleanup of fleecing images to cleanup method Fiona Ebner
2024-11-12  9:26   ` [pve-devel] applied: " Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [PATCH qemu-server v3 18/34] backup: cleanup: check if VM is running before issuing QMP commands Fiona Ebner
2024-11-12  9:26   ` [pve-devel] applied: " Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [PATCH qemu-server v3 19/34] backup: keep track of block-node size for fleecing Fiona Ebner
2024-11-11 14:22   ` Fabian Grünbichler
2024-11-12  9:50     ` Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC qemu-server v3 20/34] backup: allow adding fleecing images also for EFI and TPM Fiona Ebner
2024-11-12  9:26   ` Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [RFC qemu-server v3 21/34] backup: implement backup for external providers Fiona Ebner
2024-11-12 12:27   ` Fabian Grünbichler
2024-11-12 14:35     ` Fiona Ebner
2024-11-12 15:17       ` Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [PATCH qemu-server v3 22/34] restore: die early when there is no size for a device Fiona Ebner
2024-11-12  9:28   ` [pve-devel] applied: " Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [RFC qemu-server v3 23/34] backup: implement restore for external providers Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC qemu-server v3 24/34] backup restore: external: hardening check for untrusted source image Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [PATCH container v3 25/34] create: add missing include of PVE::Storage::Plugin Fiona Ebner
2024-11-12 15:22   ` [pve-devel] applied: " Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [RFC container v3 26/34] backup: implement backup for external providers Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC container v3 27/34] create: factor out tar restore command helper Fiona Ebner
2024-11-12 16:28   ` Fabian Grünbichler
2024-11-12 17:08   ` [pve-devel] applied: " Thomas Lamprecht
2024-11-07 16:51 ` [pve-devel] [RFC container v3 28/34] backup: implement restore for external providers Fiona Ebner
2024-11-12 16:27   ` Fabian Grünbichler
2024-11-07 16:51 ` [pve-devel] [RFC container v3 29/34] external restore: don't use 'one-file-system' tar flag when restoring from a directory Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC container v3 30/34] create: factor out compression option helper Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC container v3 31/34] restore tar archive: check potentially untrusted archive Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC container v3 32/34] api: add early check against restoring privileged container from external source Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [PATCH manager v3 33/34] ui: backup: also check for backup subtype to classify archive Fiona Ebner
2024-11-07 16:51 ` [pve-devel] [RFC manager v3 34/34] backup: implement backup for external providers Fiona Ebner
2024-11-12 15:50 ` [pve-devel] partially-applied: [RFC qemu/common/storage/qemu-server/container/manager v3 00/34] backup provider API 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=1731489757.shva7ho8xr.astroid@yuna.none \
    --to=f.gruenbichler@proxmox.com \
    --cc=pve-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