all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Max Carrara <m.carrara@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v1 pve-common 08/18] pbsclient: document package and its public functions & methods
Date: Fri,  2 Aug 2024 15:26:46 +0200	[thread overview]
Message-ID: <20240802132656.270077-9-m.carrara@proxmox.com> (raw)
In-Reply-To: <20240802132656.270077-1-m.carrara@proxmox.com>

This commit adds a brief overview for the `PVE::PBSClient` package and
documents its public functions and methods. Examples are added where
deemed appropriate.

Signed-off-by: Max Carrara <m.carrara@proxmox.com>
---
 src/PVE/PBSClient.pm | 526 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 511 insertions(+), 15 deletions(-)

diff --git a/src/PVE/PBSClient.pm b/src/PVE/PBSClient.pm
index 231406a..e0468d3 100644
--- a/src/PVE/PBSClient.pm
+++ b/src/PVE/PBSClient.pm
@@ -1,5 +1,4 @@
 package PVE::PBSClient;
-# utility functions for interaction with Proxmox Backup client CLI executable
 
 use strict;
 use warnings;
@@ -13,14 +12,83 @@ use POSIX qw(mkfifo strftime ENOENT);
 use PVE::JSONSchema qw(get_standard_option);
 use PVE::Tools qw(run_command file_set_contents file_get_contents file_read_firstline $IPV6RE);
 
-# returns a repository string suitable for proxmox-backup-client, pbs-restore, etc.
-# $scfg must have the following structure:
-# {
-#     datastore
-#     server
-#     port        (optional defaults to 8007)
-#     username    (optional defaults to 'root@pam')
-# }
+=pod
+
+=head1 NAME
+
+PVE::PBSClient - Proxmox Backup Client Library
+
+=head1 DESCRIPTION
+
+This package contains utilities that wrap common Proxmox Backup client CLI
+operations.
+
+=head2 THE CLIENT OBJECT
+
+While the C<L<PVE::PBSClient>> package contains regular L<functions|/FUNCTIONS>,
+the majority is done via the C<L<PVE::PBSClient>> object. This object represents
+a client that is used to connect to a Proxmox Backup Server:
+
+    use strict;
+    use warnings;
+
+    use Data::Dumper;
+
+    $Data::Dumper::Indent = 1;
+    $Data::Dumper::Quotekeys = 0;
+    $Data::Dumper::Sortkeys = 1;
+    $Data::Dumper::Terse = 1;
+
+    use PVE::PBSClient;
+
+    my $scfg = {
+	server => 'example.tld',
+	username => 'alice@pam',
+	datastore => 'foo-store',
+	fingerprint => '...',
+    };
+
+    my $client = PVE::PBSClient->new($scfg, "pbs-main");
+
+    my ($total, $free, $used) = $client->status();
+
+    print "Datastore has a total capacity of $total bytes, of which $used bytes are used"
+	. "and $free bytes are still available.\n";
+
+    my $snapshot = "vm/1337/2024-07-30T10:01:57Z";
+    my $filepath = "/";
+
+    my $file_list = $client->file_restore_list($snapshot, $filepath);
+
+    print "The snapshot '$snapshot' contains the following restorable files:\n";
+    print Dumper($file_list);
+    print "\n";
+
+
+=head1 FUNCTIONS
+
+=cut
+
+=pod
+
+=head3 get_repository
+
+    $repository = get_repository($scfg)
+
+Returns a repository string suitable for the C<proxmox-backup-client> and
+C<proxmox-file-restore> executables.
+
+The C<$scfg> hash must have the following structure:
+
+    {
+	datastore => 'my-datastore-name',
+	server => 'example.tld',
+	port => 8007,             # optional, defaults to 8007
+	username => 'user@realm', # optional, defaults to 'root@pam'
+    }
+
+=cut
+
 sub get_repository {
     my ($scfg) = @_;
 
@@ -41,6 +109,58 @@ sub get_repository {
     return "$username\@$server:$datastore";
 }
 
+=pod
+
+=head1 METHODS
+
+=cut
+
+=pod
+
+=head3 new
+
+    $client = PVE::PBSClient->new($scfg, $storeid)
+    $client = PVE::PBSClient->new($scfg, $storeid, $secret_dir)
+
+Creates a new instance of a C<L<PVE::PBSClient>>.
+
+Throws an exception if no C<$scfg> hash is provided or if C<$storeid> is C<undef>.
+
+=over
+
+=item C<$scfg>
+
+The I<storage config> hash that the client should use.
+
+This hash is expected to have the following structure:
+
+    {
+	datastore => 'my-datastore-name',
+	namespace => 'my-namespace',
+	server => 'example.tld',
+	fingerprint => '...',
+	port => 8007,             # optional, defaults to 8007
+	username => 'user@realm', # optional, defaults to 'root@pam'
+    }
+
+=item C<$storeid>
+
+The I<ID> of the storage corresponding to C<$scfg>. This ID is used for operations
+concerning the I<password> and I<encryption key>, such as C<L</get_password>> and
+C<L</set_encryption_key>>.
+
+=item C<$secret_dir> (optional)
+
+The name of the I<secret directory> in which the I<password> and I<encryption key>
+files are stored. Defaults to C</etc/pve/priv/storage>.
+
+Note that the I<password> and I<encryption key> files are expected to be named
+C<foo.pw> and C<foo.enc> respectively, if, for example, C<$storeid> is C<"foo">.
+
+=back
+
+=cut
+
 sub new {
     my ($class, $scfg, $storeid, $secret_dir) = @_;
 
@@ -63,6 +183,21 @@ my sub password_file_name {
     return "$self->{secret_dir}/$self->{storeid}.pw";
 }
 
+=pod
+
+=head3 set_password
+
+    $client->set_password($password)
+
+Updates or creates the I<password> file, storing the given C<$password>.
+
+If the I<secret directory> does not exist, it is created beforehand.
+
+If the I<password> file does not exist, a new one with the permissions C<600>
+is created.
+
+=cut
+
 sub set_password {
     my ($self, $password) = @_;
 
@@ -72,6 +207,19 @@ sub set_password {
     PVE::Tools::file_set_contents($pwfile, "$password\n", 0600);
 };
 
+=pod
+
+=head3 delete_password
+
+    $client->delete_password()
+
+Deletes the I<password> file inside the I<secret directory>.
+
+Will throw an exception if deleting the I<password> file fails, but not
+if the file doesn't exist.
+
+=cut
+
 sub delete_password {
     my ($self) = @_;
 
@@ -83,6 +231,16 @@ sub delete_password {
     }
 };
 
+=pod
+
+=head3 get_password
+
+    $password = $client->get_password()
+
+Reads and returns the I<password> from its file inside the I<secret directory>.
+
+=cut
+
 sub get_password {
     my ($self) = @_;
 
@@ -91,12 +249,38 @@ sub get_password {
     return PVE::Tools::file_read_firstline($pwfile);
 }
 
+=pod
+
+=head3 encryption_key_file_name
+
+    $file_name = $self->encryption_key_file_name()
+
+Returns the full name of the I<encryption key> file, including the path of the
+I<secret directory> it is located in.
+
+=cut
+
 sub encryption_key_file_name {
     my ($self) = @_;
 
     return "$self->{secret_dir}/$self->{storeid}.enc";
 };
 
+=pod
+
+=head3 set_encryption_key
+
+    $client->set_encryption_key($key)
+
+Updates or creates the I<encryption key> file, storing the given C<$key>.
+
+If the I<secret directory> does not exist, it is created beforehand.
+
+If the I<encryption key> file does not exist, a new one with the permissions C<600>
+is created.
+
+=cut
+
 sub set_encryption_key {
     my ($self, $key) = @_;
 
@@ -106,6 +290,19 @@ sub set_encryption_key {
     PVE::Tools::file_set_contents($encfile, "$key\n", 0600);
 };
 
+=pod
+
+=head3 delete_encryption_key
+
+    $client->delete_encryption_key()
+
+Deletes the I<encryption key> file inside the I<secret directory>.
+
+Will throw an exception if deleting the I<encryption key> file fails, but not
+if the file doesn't exist.
+
+=cut
+
 sub delete_encryption_key {
     my ($self) = @_;
 
@@ -235,6 +432,21 @@ my sub run_client_cmd : prototype($$;$$$$) {
     return $res;
 }
 
+=pod
+
+=head3 autogen_encryption_key
+
+    $new_key = $client->autogen_encryption_key()
+
+Generates a new I<encryption key> and stores it as a file inside the I<secret directory>.
+The raw contents of the key file, B<which are encoded as JSON string>, are
+returned afterwards.
+
+If an I<encryption key> file already exists at its expected location, an
+exception is thrown.
+
+=cut
+
 sub autogen_encryption_key {
     my ($self) = @_;
     my $encfile = $self->encryption_key_file_name();
@@ -257,7 +469,61 @@ my sub split_namespaced_parameter : prototype($$) {
     return ($namespace, $snapshot);
 }
 
-# lists all snapshots, optionally limited to a specific group
+=pod
+
+=head3 get_snapshots
+
+    $snapshots = $client->get_snapshots()
+    $snapshots = $client->get_snapshots($group)
+
+Returns all snapshots of the current client instance as a list of nesteded hashes.
+
+Optionally, the snapshots may be filtered by their C<$group>, such as C<"vm/100">
+or C<"ct/2000">, for example.
+
+The returned list has the following structure:
+
+    [
+	{
+	    'backup-id' => "100",
+	    'backup-time' => 1721901601,
+	    'backup-type' => "vm",
+	    comment => "standalone node/example-host/100-example-vm",
+	    files: [
+		{
+		    'crypt-mode' => "encrypt",
+		    filename => "qemu-server.conf.blob",
+		    size => 428
+		},
+		{
+		    'crypt-mode' => "encrypt",
+		    filename => "drive-scsi0.img.fidx",
+		    size => 17179869184
+		},
+		{
+		    'crypt-mode' => "sign-only",
+		    filename => "index.json.blob",
+		    size => 651
+		},
+		{
+		    filename => "client.log.blob"
+		},
+		...
+	    ],
+	    fingerprint => "...",
+	    owner => "root@pam",
+	    protected => false,
+	    size => 17179870263,
+	    verification => {
+		state => "ok",
+		upid => "..."
+	    }
+	},
+	...
+    ]
+
+=cut
+
 sub get_snapshots {
     my ($self, $group) = @_;
 
@@ -274,8 +540,28 @@ sub get_snapshots {
     return run_client_cmd($self, "snapshots", $param, undef, undef, $namespace);
 };
 
-# create a new PXAR backup of a FS directory tree - doesn't cross FS boundary
-# by default.
+=pod
+
+=head3 backup_fs_tree
+
+    $client->backup_fs_tree($root, $id, $pxarname)
+    $client->backup_fs_tree($root, $id, $pxarname, $cmd_opts)
+
+Create a new PXAR backup of a directory tree starting at C<$root>.
+
+C<$id> is the I<ID> of the stored backup and C<$pxarname> is the name of the
+uploaded PXAR archive.
+
+Optionally, the C<$cmd_opts> hash may be supplied, which should contain
+additional parameters to pass to C<L<PVE::Tools::run_command>> that is used
+under the hood.
+
+Raises an exception if either C<$root>, C<$id> or C<$pxarname> is C<undef>.
+
+B<NOTE:> This does B<not> cross filesystem boundaries.
+
+=cut
+
 sub backup_fs_tree {
     my ($self, $root, $id, $pxarname, $cmd_opts) = @_;
 
@@ -298,6 +584,32 @@ sub backup_fs_tree {
     return run_raw_client_cmd($self, 'backup', $param, %$cmd_opts);
 };
 
+=pod
+
+=head3 restore_pxar
+
+    $client->restore_pxar($snapshot, $pxarname, $target)
+    $client->restore_pxar($snapshot, $pxarname, $target, $cmd_opts)
+
+Restore a PXAR backup of a directory tree from the given C<$snapshot>.
+
+C<$pxarname> is the name of the previously uploaded PXAR archive to restore and
+C<$target> the directory to which the backed up tree will be restored to.
+
+Note that C<$snapshot> must be the snapshot's complete name in the format
+C<TYPE/ID/BACKUP_TIME> - for example C<"vm/100/2023-07-31T16:00:00Z"> or
+C<"ct/2000/2024-08-01T09:54:08Z"> (like it's displayed in the PBS UI).
+
+Optionally, the C<$cmd_opts> hash may be supplied, which should contain
+additional parameters to pass to C<L<PVE::Tools::run_command>> that is used
+under the hood.
+
+Raises an exception if either C<$snapshot>, C<$pxarname> or C<$target> is C<undef>,
+or if a filesystem entry to be restored already exists inside the C<$target>
+directory.
+
+=cut
+
 sub restore_pxar {
     my ($self, $snapshot, $pxarname, $target, $cmd_opts) = @_;
 
@@ -320,6 +632,22 @@ sub restore_pxar {
     return run_raw_client_cmd($self, 'restore', $param, %$cmd_opts);
 };
 
+=pod
+
+=head3 forget_snapshot
+
+    $client->forget_snapshot($snapshot)
+
+Forgets the given C<$snapshot>.
+
+Note that C<$snapshot> must be the snapshot's complete name in the format
+C<TYPE/ID/BACKUP_TIME> - for example C<"vm/100/2023-07-31T16:00:00Z"> or
+C<"ct/2000/2024-08-01T09:54:08Z"> (as displayed in the PBS UI).
+
+Raises an exception if C<$snapshot> is C<undef>.
+
+=cut
+
 sub forget_snapshot {
     my ($self, $snapshot) = @_;
 
@@ -330,6 +658,41 @@ sub forget_snapshot {
     return run_client_cmd($self, 'forget', [ "$snapshot" ], 1, undef, $namespace)
 };
 
+=pod
+
+=head3 prune_group
+
+    $client->prune_group($opts, $prune_opts, $group)
+
+Prunes a backup C<$group>. The exact behaviour can be controlled using the
+C<$opts> and C<$prune_opts> hashes.
+
+C<$group> must be in the format of C<TYPE/ID>, like C<"vm/100"> or C<"ct/2000">,
+for example (as displayed in the PBS UI).
+
+The C<$opts> hash supports the following options and may be left empty:
+
+    {
+	'dry-run' => 1,  # perform a dry run
+    }
+
+The C<$prune_opts> hash supports the following options:
+
+    {
+	'keep-last' => 1,
+	'keep-hourly' => 1,
+	'keep-daily' => 1,
+	'keep-weekly' => 1,
+	'keep-monthly' => 1,
+	'keep-yearly' => 1,
+    }
+
+Will do nothing if no C<$prune_opts> are supplied.
+
+Raises an exception if C<$group> is C<undef>.
+
+=cut
+
 sub prune_group {
     my ($self, $opts, $prune_opts, $group) = @_;
 
@@ -356,6 +719,19 @@ sub prune_group {
     return run_client_cmd($self, 'prune', $param, undef, undef, $namespace);
 };
 
+=pod
+
+=head3 status
+
+    ($total, $free, $used, $active) = $client->status()
+
+Return the I<status> of the client's repository as an array.
+
+The array contains the C<$total>, C<$free> and C<$used> size of the repository
+as bytes, as well as whether the repository is C<$active> or not.
+
+=cut
+
 sub status {
     my ($self) = @_;
 
@@ -379,6 +755,59 @@ sub status {
     return ($total, $free, $used, $active);
 };
 
+=pod
+
+=head3 file_restore_list
+
+    $restore_list = $client->($snapshot, $filepath)
+    $restore_list = $client->($snapshot, $filepath, $base64)
+    $restore_list = $client->($snapshot, $filepath, $base64, $extra_params)
+
+Return the list of entries from a directory C<$filepath> of a backup C<$snapshot>.
+
+Note that C<$snapshot> must be the snapshot's complete name in the format
+C<TYPE/ID/BACKUP_TIME> - for example C<"vm/100/2023-07-31T16:00:00Z"> or
+C<"ct/2000/2024-08-01T09:54:08Z"> (as displayed in the PBS UI).
+
+C<$base64> may optionally be set to C<1> if the C<$filepath> is base64-encoded.
+
+The C<$extra_params> hash supports the following options and may be left empty:
+
+    {
+	timeout => 5,  # in seconds
+    }
+
+If successful, the returned list of hashes has the following structure:
+
+    [
+	{
+	    filepath => "L2RyaXZlLXNjc2kwLmltZy5maWR4",
+	    leaf => 0,
+	    size => 34359738368,
+	    text => "drive-scsi0.img.fidx",
+	    type => "v"
+	},
+	{
+	    filepath => "L2RyaXZlLXNjc2kxLmltZy5maWR4",
+	    leaf => 0,
+	    size => 68719476736,
+	    text => "drive-scsi1.img.fidx",
+	    type => "v"
+	},
+	...
+    ]
+
+On error, the list of hashes will contain an error message, for example:
+
+    [
+	{
+	    message => "wrong key - unable to verify signature since manifest's key [...] does not match provided key [...]"
+	},
+	...
+    ]
+
+=cut
+
 sub file_restore_list {
     my ($self, $snapshot, $filepath, $base64, $extra_params) = @_;
 
@@ -399,8 +828,32 @@ sub file_restore_list {
     );
 }
 
-# call sync from API, returns a fifo path for streaming data to clients,
-# pass it to file_restore_extract to start transfering data
+=pod
+
+=head3 file_restore_extract_prepare
+
+    $fifo = $client->file_restore_extract_prepare()
+
+Create a I<named pipe> (FIFO) for streaming data and return its path.
+
+The returned path is usually passed to C<L</file_restore_extract>> which will
+stream the data to the I<named pipe>. A different process may then read from the
+same path, receiving the streamed data.
+
+Raises an exception if:
+
+=over
+
+=item creating the I<named pipe> fails
+
+=item the call to C<getpwnam> for the C<www-data> user fails
+
+=item changing the permissions for the I<named pipe> or its directory fails
+
+=back
+
+=cut
+
 sub file_restore_extract_prepare {
     my ($self) = @_;
 
@@ -419,7 +872,50 @@ sub file_restore_extract_prepare {
     return "$tmpdir/fifo";
 }
 
-# this blocks while data is transfered, call this from a background worker
+=pod
+
+=head3 file_restore_extract
+
+    $client->file_restore_extract($output_file, $snapshot, $filepath, $base64, $tar)
+
+Restores and extracts a C<$filepath> from a C<$snapshot> to the given C<$output_file>.
+By default, the C<$output_file> will be a ZIP archive.
+
+Because this method is mostly used for streaming purposes in conjunction with
+C<L</file_restore_extract_prepare>>, the C<$output_file> will be automatically
+I<unlinked> once the extraction is complete. See below for an example on how to
+use this method.
+
+C<$base64> may optionally be set to C<1> if the C<$filepath> is base64-encoded.
+
+C<$tar> may optionally be set to C<1> if the output written to C<$output_file>
+should be a ZSTD-compressed TAR archive. B<Otherwise, the file will be saved as
+a ZIP archive.>
+
+Usually used in conjunction with C<L</file_restore_extract_prepare>>.
+
+B<NOTE:> This method B<blocks> while data is being transferred to C<$output_file>.
+It is therefore best to call this within C<L<PVE::RESTEnvironment::fork_worker>>
+or C<L<PVE::RPCEnvironment::fork_worker>>, for example:
+
+    # [...]
+    my $fifo = $client->file_restore_extract_prepare();
+
+    $rpcenv->fork_worker('backup-download', undef, $user, sub {
+	print "Starting download of file: $filename\n";
+	$client->file_restore_extract($fifo, $snapshot, $filepath, 0, $tar);
+    });
+
+    return {
+	download => {
+	    path => $fifo,
+	    stream => 1,
+	    'content-type' => 'application/octet-stream',
+	},
+    };
+
+=cut
+
 sub file_restore_extract {
     my ($self, $output_file, $snapshot, $filepath, $base64, $tar) = @_;
 
-- 
2.39.2



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


  parent reply	other threads:[~2024-08-02 13:28 UTC|newest]

Thread overview: 35+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-08-02 13:26 [pve-devel] [PATCH v1 pve-common 00/18] Introduction of libproxmox-backup-client-perl Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 01/18] pbsclient: rename 'sdir' parameter of constructor to 'secret_dir' Max Carrara
2024-11-11 19:08   ` [pve-devel] applied: " Thomas Lamprecht
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 02/18] pbsclient: use parentheses when calling most inbuilts Max Carrara
2024-11-11 19:08   ` [pve-devel] applied: " Thomas Lamprecht
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 03/18] pbsclient: use post-if definedness checks instead of '//=' operator Max Carrara
2024-11-11 19:09   ` [pve-devel] applied: " Thomas Lamprecht
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 04/18] pbsclient: pull variable out of long post-if definedness check Max Carrara
2024-11-11 19:09   ` [pve-devel] applied: " Thomas Lamprecht
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 05/18] pbsclient: use cond. statements instead of chained 'or' operators Max Carrara
2024-11-11 19:10   ` Thomas Lamprecht
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 06/18] pbsclient: use spaces around list braces and parens around ternaries Max Carrara
2024-11-11 19:11   ` [pve-devel] applied: " Thomas Lamprecht
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 07/18] pbsclient: s/foreach/for Max Carrara
2024-11-11 19:11   ` [pve-devel] applied: " Thomas Lamprecht
2024-08-02 13:26 ` Max Carrara [this message]
2024-11-11 19:14   ` [pve-devel] [PATCH v1 pve-common 08/18] pbsclient: document package and its public functions & methods Thomas Lamprecht
2024-11-21 15:54     ` Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 09/18] pbsclient: create secret dir with `mkdir -p` and mode `700` Max Carrara
2024-11-11 19:16   ` Thomas Lamprecht
2024-11-21 16:09     ` Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 10/18] pbsclient: use `File::Spec->catfile` to concatenate file paths Max Carrara
2024-11-11 19:16   ` Thomas Lamprecht
2024-11-21 16:17     ` Max Carrara
2024-11-22 12:46       ` Thomas Lamprecht
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 11/18] pbsclient: let `status` method return a hash instead of an array Max Carrara
2024-11-11 19:17   ` Thomas Lamprecht
2024-11-21 16:29     ` Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 12/18] pbsclient: throw exception if username of client has no realm Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 13/18] pbsclient: make method `password_file_name` public Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 14/18] pbsclient: prohibit implicit return Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 15/18] pbsclient: don't return anything in PXAR methods Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 16/18] pbsclient: don't return anything in `forget_snapshot` Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 17/18] make: support building multiple packages from the same source Max Carrara
2024-08-02 13:26 ` [pve-devel] [PATCH v1 pve-common 18/18] deb: split PBSClient.pm into new package libproxmox-backup-client-perl Max Carrara

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=20240802132656.270077-9-m.carrara@proxmox.com \
    --to=m.carrara@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 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