* [pve-devel] [PATCH v2 pve-storage-plugin-examples 1/2] plugin-sshfs: add example for custom storage plugin for SSHFS
2025-07-04 16:20 [pve-devel] [PATCH v2 pve-storage-plugin-examples 0/2] SSHFS Example Storage Plugin Max R. Carrara
@ 2025-07-04 16:20 ` Max R. Carrara
2025-07-04 16:20 ` [pve-devel] [PATCH v2 pve-storage-plugin-examples 2/2] plugin-sshfs: package the SSHFS example plugin Max R. Carrara
2025-09-08 8:26 ` [pve-devel] applied: [PATCH v2 pve-storage-plugin-examples 0/2] SSHFS Example Storage Plugin Thomas Lamprecht
2 siblings, 0 replies; 4+ messages in thread
From: Max R. Carrara @ 2025-07-04 16:20 UTC (permalink / raw)
To: pve-devel
This commit adds an example implementation of a custom storage plugin
that uses SSHFS [0] as the underlying filesystem.
The implementation is very similar to that of the NFS plugin; as a
prerequisite, it is currently necessary to use pubkey auth and have
the host's root user's public key deployed on the remote host like so:
ssh-copy-id -i ~/.ssh/id_my_private_key [USER]@[HOST]
Then, the storage can be added as follows:
pvesm add sshfs [STOREID] \
--username [USER] \
--server [HOST] \
--path /mnt/path/to/storage \
--remote-path [ABS PATH ON REMOTE] \
--sshfs-private-key "$(cat ~/.ssh/id_my_private_key)"
If the host is part of a cluster, other nodes may connect to the
remote without any additional setup required. This is because we copy
the private key to `/etc/pve/priv/storage/$STOREID.key` and use
`/etc/pve/priv/storage/$STOREID_known_hosts` as cluster-wide
known_hosts file. Also mark each SSHFS storage as `shared` by default
in order to make use of this.
In other words, there will be a separate private key and known_hosts
file for each SSHFS storage that the user configures. Both are shared
via pmxcfs.
Note: Because there's currently no way to officially and permanently
mark a storage as shared (like some built-in plugins [1]) set
`$scfg->{shared} = 1;` in `on_add_hook`. This has almost the same
effect as modifying `@PVE::Storage::Plugin::SHARED_STORAGE` directly,
except that the `shared` is written to `/etc/pve/storage.cfg`.
The remote host's public key is trusted on first use via the
StrictHostKeyChecking=accept-new option for SSH.
From `man 5 ssh_config`:
> If this flag is set to accept-new then ssh will automatically add
> new host keys to the user's known_hosts file, but will not permit
> connections to hosts with changed host keys.
This means that `/etc/pve/priv/storage/$STOREID_known_hosts` will
contain the public key of the remote host after a connection was
established for the first time.
[0]: https://github.com/libfuse/sshfs
[1]: https://git.proxmox.com/?p=pve-storage.git;a=blob;f=src/PVE/Storage/Plugin.pm;h=4e16420f667f196e8eb99ae7c9f3f1d3e13791fb;hb=refs/heads/master#l37
Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
.../src/PVE/Storage/Custom/SSHFSPlugin.pm | 395 ++++++++++++++++++
1 file changed, 395 insertions(+)
create mode 100644 plugin-sshfs/src/PVE/Storage/Custom/SSHFSPlugin.pm
diff --git a/plugin-sshfs/src/PVE/Storage/Custom/SSHFSPlugin.pm b/plugin-sshfs/src/PVE/Storage/Custom/SSHFSPlugin.pm
new file mode 100644
index 0000000..b91053f
--- /dev/null
+++ b/plugin-sshfs/src/PVE/Storage/Custom/SSHFSPlugin.pm
@@ -0,0 +1,395 @@
+package PVE::Storage::Custom::SSHFSPlugin;
+
+use v5.36;
+
+use Cwd qw();
+use Encode qw(decode encode);
+use File::Path qw(make_path);
+use File::Basename qw(dirname);
+use IO::File;
+use POSIX qw(:errno_h);
+
+use PVE::ProcFSTools;
+use PVE::Tools qw(
+ file_copy
+ file_get_contents
+ file_set_contents
+ run_command
+);
+
+use base qw(PVE::Storage::Plugin);
+
+# Plugin Definition
+
+sub api {
+ return 11;
+}
+
+sub type {
+ return 'sshfs';
+}
+
+sub plugindata {
+ return {
+ content => [
+ {
+ images => 1,
+ rootdir => 1,
+ vztmpl => 1,
+ iso => 1,
+ backup => 1,
+ snippets => 1,
+ },
+ {
+ images => 1,
+ rootdir => 1,
+ },
+ ],
+ format => [
+ {
+ raw => 1,
+ qcow2 => 1,
+ vmdk => 1,
+ },
+ 'qcow2',
+ ],
+ 'sensitive-properties' => {
+ 'sshfs-private-key' => 1,
+ },
+ };
+}
+
+sub properties {
+ return {
+ 'remote-path' => {
+ description => "Path on the remote filesystem used for SSHFS. Must be absolute.",
+ type => 'string',
+ format => 'pve-storage-path',
+ },
+ 'sshfs-private-key' => {
+ description => "The private key to use for SSHFS.",
+ type => 'string',
+ },
+ };
+}
+
+sub options {
+ return {
+ disable => { optional => 1 },
+ path => { fixed => 1 },
+ 'create-base-path' => { optional => 1 },
+ content => { optional => 1 },
+ 'create-subdirs' => { optional => 1 },
+ 'content-dirs' => { optional => 1 },
+ 'prune-backups' => { optional => 1 },
+ 'max-protected-backups' => { optional => 1 },
+ format => { optional => 1 },
+ bwlimit => { optional => 1 },
+ preallocation => { optional => 1 },
+ nodes => { optional => 1 },
+ shared => { optional => 1 },
+
+ # SSHFS Options
+ username => {},
+ server => {},
+ 'remote-path' => {},
+ port => { optional => 1 },
+ 'sshfs-private-key' => { optional => 1 },
+ };
+}
+
+# SSHFS Helpers
+
+my sub sshfs_remote_from_config : prototype($) ($scfg) {
+ my ($user, $host, $remote_path) = $scfg->@{qw(username server remote-path)};
+ return "${user}\@${host}:${remote_path}";
+}
+
+my sub sshfs_private_key_path : prototype($) ($storeid) {
+ return "/etc/pve/priv/storage/$storeid.key";
+}
+
+my sub sshfs_known_hosts_path : prototype($) ($storeid) {
+ return "/etc/pve/priv/storage/${storeid}_known_hosts";
+}
+
+my sub sshfs_common_ssh_opts : prototype($) ($storeid) {
+ my $private_key_path = sshfs_private_key_path($storeid);
+ my $known_hosts_path = sshfs_known_hosts_path($storeid);
+
+ my @common_opts = (
+ '-o',
+ "UserKnownHostsFile=${known_hosts_path}",
+ '-o',
+ 'GlobalKnownHostsFile=none',
+ '-o',
+ "IdentityFile=${private_key_path}",
+ '-o',
+ "StrictHostKeyChecking=accept-new",
+ );
+
+ return @common_opts;
+}
+
+my sub sshfs_set_private_key : prototype($$) ($storeid, $key) {
+ my $key_path = sshfs_private_key_path($storeid);
+
+ my $key_parent_dir = dirname($key_path);
+ if (!-e $key_parent_dir) {
+ # We don't need to set the mode here as pmxcfs sets it itself
+ make_path($key_parent_dir);
+ }
+
+ # Same here; pmxcfs sets the mode itself, so we don't need to here
+ file_set_contents($key_path, "$key\n");
+
+ return undef;
+}
+
+my sub sshfs_remove_private_key : prototype($) ($storeid) {
+ my $key_path = sshfs_private_key_path($storeid);
+
+ if (!unlink $key_path) {
+ return if $! == ENOENT;
+ die "failed to remove private key '$key_path' - $!\n";
+ }
+
+ return undef;
+}
+
+my sub sshfs_remove_known_hosts : prototype($) ($storeid) {
+ my $known_hosts_path = sshfs_known_hosts_path($storeid);
+
+ if (!unlink $known_hosts_path) {
+ return if $! == ENOENT;
+ die "failed to remove known_hosts file '$known_hosts_path' - $!\n";
+ }
+
+ return undef;
+}
+
+my sub sshfs_is_mounted : prototype($) ($scfg) {
+ my $remote = sshfs_remote_from_config($scfg);
+
+ my $mountpoint = Cwd::realpath($scfg->{path}); # Resolve symlinks
+ return 0 if !defined($mountpoint);
+
+ my $mountdata = PVE::ProcFSTools::parse_proc_mounts();
+
+ my $has_found_mountpoint = grep {
+ $_->[0] =~ m|^\Q${remote}\E$|
+ && $_->[1] eq $mountpoint
+ && $_->[2] eq 'fuse.sshfs'
+ } $mountdata->@*;
+
+ return $has_found_mountpoint != 0;
+}
+
+my sub sshfs_mount : prototype($$) ($scfg, $storeid) {
+ my $remote = sshfs_remote_from_config($scfg);
+ my ($port, $mountpoint) = $scfg->@{qw(port path)};
+
+ my @common_opts = sshfs_common_ssh_opts($storeid);
+ my $cmd = [
+ '/usr/bin/sshfs', @common_opts, '-o', 'noatime',
+ ];
+
+ push($cmd->@*, '-p', $port) if $port;
+ push($cmd->@*, $remote, $mountpoint);
+
+ eval {
+ run_command(
+ $cmd,
+ timeout => 10,
+ errfunc => sub { warn "$_[0]\n"; },
+ );
+ };
+ if (my $err = $@) {
+ die "failed to mount SSHFS storage '$remote' at '$mountpoint': $@\n";
+ }
+
+ die "SSHFS storage '$remote' not mounted at '$mountpoint' despite reported success\n"
+ if !sshfs_is_mounted($scfg);
+
+ return;
+}
+
+my sub sshfs_umount : prototype($) ($scfg) {
+ my $mountpoint = $scfg->{path};
+
+ my $cmd = ['/usr/bin/umount', $mountpoint];
+
+ eval {
+ run_command(
+ $cmd,
+ timeout => 10,
+ errfunc => sub { warn "$_[0]\n"; },
+ );
+ };
+ if (my $err = $@) {
+ die "failed to unmount SSHFS at '$mountpoint': $err\n";
+ }
+
+ return;
+}
+
+# Storage Implementation
+
+sub on_add_hook($class, $storeid, $scfg, %sensitive) {
+ $scfg->{shared} = 1; # mark SSHFS storages as shared by default
+
+ eval {
+ if (defined(my $key = delete $sensitive{'sshfs-private-key'})) {
+ sshfs_set_private_key($storeid, $key);
+ }
+ };
+ die "error while adding SSHFS storage '${storeid}': $@\n" if $@;
+
+ return undef;
+}
+
+sub on_update_hook($class, $storeid, $scfg, %sensitive) {
+ if (exists($sensitive{'sshfs-private-key'})) {
+ if (defined($sensitive{'sshfs-private-key'})) {
+ eval { sshfs_set_private_key($storeid, $sensitive{'sshfs-private-key'}) };
+ die $@ if $@;
+
+ } else {
+ warn "removing private key for SSHFS storage '${storeid}'";
+ warn "the storage might not be mountable without a private key!";
+
+ eval { sshfs_remove_private_key($storeid); };
+ die $@ if $@;
+ }
+ }
+
+ return undef;
+}
+
+sub on_delete_hook($class, $storeid, $scfg) {
+ eval { sshfs_remove_private_key($storeid); };
+ warn $@ if $@;
+
+ eval { sshfs_remove_known_hosts($storeid); };
+ warn $@ if $@;
+
+ eval { sshfs_umount($scfg) if sshfs_is_mounted($scfg); };
+ warn $@ if $@;
+
+ return undef;
+}
+
+sub check_connection($class, $storeid, $scfg) {
+ my ($user, $host, $port) = $scfg->@{qw(username server port)};
+
+ my @common_opts = sshfs_common_ssh_opts($storeid);
+ my $cmd = [
+ '/usr/bin/ssh', '-T', @common_opts, '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5',
+ ];
+
+ push($cmd->@*, "-p", $port) if $port;
+ push($cmd->@*, "${user}\@${host}", 'exit 0');
+
+ eval {
+ run_command(
+ $cmd,
+ timeout => 10,
+ errfunc => sub { warn "$_[0]\n"; },
+ );
+ };
+ if (my $err = $@) {
+ warn "$err";
+ return 0;
+ }
+
+ return 1;
+}
+
+sub activate_storage($class, $storeid, $scfg, $cache) {
+ my $mountpoint = $scfg->{path};
+
+ if (!sshfs_is_mounted($scfg)) {
+ if ($scfg->{'create-base-path'} // 1) {
+ make_path($mountpoint);
+ }
+
+ die "unable to activate storage '$storeid' - directory '$mountpoint' does not exist\n"
+ if !-d $mountpoint;
+
+ sshfs_mount($scfg, $storeid);
+ }
+
+ $class->SUPER::activate_storage($storeid, $scfg, $cache);
+ return;
+}
+
+sub get_volume_attribute($class, $scfg, $storeid, $volname, $attribute) {
+ my ($vtype) = $class->parse_volname($volname);
+ return if $vtype ne 'backup';
+
+ my $volume_path = PVE::Storage::Plugin->filesystem_path($scfg, $volname);
+
+ if ($attribute eq 'notes') {
+ my $notes_path = $volume_path . $class->SUPER::NOTES_EXT;
+
+ if (-f $notes_path) {
+ my $notes = file_get_contents($notes_path);
+ return eval { decode('UTF-8', $notes, 1) } // $notes;
+ }
+
+ return "";
+ }
+
+ if ($attribute eq 'protected') {
+ return -e PVE::Storage::protection_file_path($volume_path) ? 1 : 0;
+ }
+
+ return;
+}
+
+sub update_volume_attribute($class, $scfg, $storeid, $volname, $attribute, $value) {
+ my ($vtype, $name) = $class->parse_volname($volname);
+ die "only backups support attribute '$attribute'\n" if $vtype ne 'backup';
+
+ my $volume_path = PVE::Storage::Plugin->filesystem_path($scfg, $volname);
+
+ if ($attribute eq 'notes') {
+ my $notes_path = $volume_path . $class->SUPER::NOTES_EXT;
+
+ if (defined($value)) {
+ my $encoded_notes = encode('UTF-8', $value);
+ file_set_contents($notes_path, $encoded_notes);
+ } else {
+ if (!unlink $notes_path) {
+ return if $! == ENOENT;
+ die "could not delete notes - $!\n";
+ }
+ }
+
+ return;
+ }
+
+ if ($attribute eq 'protected') {
+ my $protection_path = PVE::Storage::protection_file_path($volume_path);
+
+ # Protection already set or unset
+ return if !((-e $protection_path) xor $value);
+
+ if ($value) {
+ my $fh = IO::File->new($protection_path, O_CREAT)
+ or die "unable to create protection file '$protection_path' - $!\n";
+ close($fh);
+ } else {
+ if (!unlink $protection_path) {
+ return if $! == ENOENT;
+ die "could not delete protection file '$protection_path' - $!\n";
+ }
+ }
+
+ return;
+ }
+
+ die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
+}
+
+1;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 4+ messages in thread
* [pve-devel] [PATCH v2 pve-storage-plugin-examples 2/2] plugin-sshfs: package the SSHFS example plugin
2025-07-04 16:20 [pve-devel] [PATCH v2 pve-storage-plugin-examples 0/2] SSHFS Example Storage Plugin Max R. Carrara
2025-07-04 16:20 ` [pve-devel] [PATCH v2 pve-storage-plugin-examples 1/2] plugin-sshfs: add example for custom storage plugin for SSHFS Max R. Carrara
@ 2025-07-04 16:20 ` Max R. Carrara
2025-09-08 8:26 ` [pve-devel] applied: [PATCH v2 pve-storage-plugin-examples 0/2] SSHFS Example Storage Plugin Thomas Lamprecht
2 siblings, 0 replies; 4+ messages in thread
From: Max R. Carrara @ 2025-07-04 16:20 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
Makefile | 1 +
plugin-sshfs/Makefile | 71 +++++++++++++++++++++++++++++++
plugin-sshfs/debian/changelog | 5 +++
plugin-sshfs/debian/control | 22 ++++++++++
plugin-sshfs/debian/copyright | 20 +++++++++
plugin-sshfs/debian/rules | 8 ++++
plugin-sshfs/debian/source/format | 1 +
plugin-sshfs/debian/triggers | 1 +
8 files changed, 129 insertions(+)
create mode 100644 plugin-sshfs/Makefile
create mode 100644 plugin-sshfs/debian/changelog
create mode 100644 plugin-sshfs/debian/control
create mode 100644 plugin-sshfs/debian/copyright
create mode 100755 plugin-sshfs/debian/rules
create mode 100644 plugin-sshfs/debian/source/format
create mode 100644 plugin-sshfs/debian/triggers
diff --git a/Makefile b/Makefile
index 1e422cc..b5c787a 100644
--- a/Makefile
+++ b/Makefile
@@ -10,6 +10,7 @@
SUBDIRS := \
backup-provider-borg \
backup-provider-directory \
+ plugin-sshfs \
.PHONY: deb dsc sbuild clean
deb:
diff --git a/plugin-sshfs/Makefile b/plugin-sshfs/Makefile
new file mode 100644
index 0000000..7a181dc
--- /dev/null
+++ b/plugin-sshfs/Makefile
@@ -0,0 +1,71 @@
+# Makes useful Debian-specific variables available
+include /usr/share/dpkg/default.mk
+
+# --- Useful variables for convenience
+
+# Note that variables can be overridden, e.g. `make install DESTDIR='./foo'`
+DESTDIR=
+PACKAGE=libpve-storage-plugin-sshfs-perl
+
+BINDIR=${DESTDIR}/usr/bin
+PERLLIBDIR=${DESTDIR}/usr/share/perl5
+MAN1DIR=${DESTDIR}/usr/share/man/man1
+MAN8DIR=${DESTDIR}/usr/share/man/man8
+CRONDAILYDIR=${DESTDIR}/etc/cron.daily
+INITDBINDIR=${DESTDIR}/etc/init.d
+SERVICEDIR=${DESTDIR}/lib/systemd/system
+BASHCOMPLDIR=${DESTDIR}/usr/share/bash-completion/completions/
+ZSHCOMPLDIR=${DESTDIR}/usr/share/zsh/vendor-completions/
+HARADIR=${DESTDIR}/usr/share/cluster
+DOCDIR=${DESTDIR}/usr/share/doc/${PACKAGE}
+PODDIR=${DESTDIR}/usr/share/doc/${PACKAGE}/pod
+USRSHARE=${DESTDIR}/usr/share/${PACKAGE}
+
+# Directory where our custom plugin has to go
+PLUGINDIR=${PERLLIBDIR}/PVE/Storage/Custom
+
+export VERSION = $(DEB_VERSION_UPSTREAM_REVISION)
+
+BUILDDIR = $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
+
+DSC=$(PACKAGE)_$(DEB_VERSION).dsc
+DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+
+# --- Targets
+
+$(BUILDDIR):
+ rm -rf $@ $@.tmp
+ mkdir $@.tmp
+ rsync -a * $@.tmp
+ # You can add additional commands instead of this comment if you need to
+ # e.g. create additional files inside the build directory etc.
+ mv $@.tmp $@
+
+# Creates a .deb package for installation
+.PHONY: deb
+deb: $(DEB)
+$(DEB): $(BUILDDIR)
+ cd $(BUILDDIR); dpkg-buildpackage -b -us -uc
+ lintian $(DEB)
+
+# Creates a .dsc (Debian source) package
+.PHONY: dsc
+dsc:
+ rm -rf $(BUILDDIR) $(DSC)
+ $(MAKE) $(DSC)
+ lintian $(DSC)
+
+$(DSC): $(BUILDDIR)
+ cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
+
+# Target used to place files and directories at expected locations, with expected permissions
+.PHONY: install
+install:
+ install -D -m 0644 src/PVE/Storage/Custom/SSHFSPlugin.pm $(PLUGINDIR)/SSHFSPlugin.pm
+
+# Used to clean up your builds
+.PHONY: clean
+clean:
+ rm -f $(PACKAGE)*.tar* *.deb *.dsc *.build *.buildinfo *.changes
+ rm -rf dest $(PACKAGE)-[0-9]*/
+
diff --git a/plugin-sshfs/debian/changelog b/plugin-sshfs/debian/changelog
new file mode 100644
index 0000000..bd243c5
--- /dev/null
+++ b/plugin-sshfs/debian/changelog
@@ -0,0 +1,5 @@
+libpve-storage-plugin-sshfs-perl (1.0.0) UNRELEASED; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 09 Apr 2025 16:13:26 +0200
diff --git a/plugin-sshfs/debian/control b/plugin-sshfs/debian/control
new file mode 100644
index 0000000..67317b0
--- /dev/null
+++ b/plugin-sshfs/debian/control
@@ -0,0 +1,22 @@
+Source: libpve-storage-plugin-sshfs-perl
+Section: perl
+Priority: optional
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Rules-Requires-Root: no
+Build-Depends:
+ debhelper-compat (= 13),
+ libpve-storage-perl (>= 8.3.3),
+ lintian,
+ perl,
+ rsync,
+Standards-Version: 4.6.2
+
+Package: libpve-storage-plugin-sshfs-perl
+Architecture: any
+Depends:
+ ${misc:Depends},
+ ${perl:Depends},
+ libpve-storage-perl (>= 8.3.3),
+ sshfs (>= 3.7.3),
+Description: SSHFS storage plugin for Proxmox Virtual Environment.
+ Used to demonstrate plugin development.
diff --git a/plugin-sshfs/debian/copyright b/plugin-sshfs/debian/copyright
new file mode 100644
index 0000000..291a5c8
--- /dev/null
+++ b/plugin-sshfs/debian/copyright
@@ -0,0 +1,20 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Source: https://git.proxmox.com/?p=pve-storage-plugin-examples.git;a=summary
+
+Files:
+ *
+Copyright: 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ .
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+ .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
diff --git a/plugin-sshfs/debian/rules b/plugin-sshfs/debian/rules
new file mode 100755
index 0000000..7f90d5d
--- /dev/null
+++ b/plugin-sshfs/debian/rules
@@ -0,0 +1,8 @@
+#!/usr/bin/make -f
+
+# Output every command that modifies files on the build system.
+#export DH_VERBOSE = 1
+
+%:
+ dh $@
+
diff --git a/plugin-sshfs/debian/source/format b/plugin-sshfs/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/plugin-sshfs/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/plugin-sshfs/debian/triggers b/plugin-sshfs/debian/triggers
new file mode 100644
index 0000000..59dd688
--- /dev/null
+++ b/plugin-sshfs/debian/triggers
@@ -0,0 +1 @@
+activate-noawait pve-api-updates
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 4+ messages in thread